From a4f4cf3bef4638dd6cc66c319a968440f60f82b0 Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Mon, 22 Sep 2025 17:27:24 -0400 Subject: [PATCH 01/19] Semi-implicit model for OGGM dynamical runs (#125) Closes #124. --- pygem/bin/run/run_simulation.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/pygem/bin/run/run_simulation.py b/pygem/bin/run/run_simulation.py index 3066befe..7bf357d2 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -42,7 +42,7 @@ pygem_prms = config_manager.read_config() # oggm imports from oggm import cfg, graphics, tasks, utils -from oggm.core.flowline import FluxBasedModel +from oggm.core.flowline import FluxBasedModel, SemiImplicitModel from oggm.core.massbalance import apparent_mb_from_any_mb import pygem.gcmbiasadj as gcmbiasadj @@ -1067,17 +1067,26 @@ def run(list_packed_vars): if debug: print('OGGM GLACIER DYNAMICS!') - # new numerical scheme is SemiImplicitModel() but doesn't have frontal ablation yet - # FluxBasedModel is old numerical scheme but includes frontal ablation - ev_model = FluxBasedModel( - nfls, - y0=args.sim_startyear, - mb_model=mbmod, - glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, - fs=fs, - is_tidewater=gdir.is_tidewater, - water_level=water_level, - ) + # FluxBasedModel is older numerical scheme but includes frontal ablation + if gdir.is_tidewater: + ev_model = FluxBasedModel( + nfls, + y0=args.sim_startyear, + mb_model=mbmod, + glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, + fs=fs, + is_tidewater=gdir.is_tidewater, + water_level=water_level, + ) + # SemiImplicitModel is newer numerical solver, but does not yet include frontal ablation + else: + ev_model = SemiImplicitModel( + nfls, + y0=args.sim_startyear, + mb_model=mbmod, + glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, + fs=fs, + ) if debug: graphics.plot_modeloutput_section(ev_model) From f5d3b730cdc7b7dc9c3ecc78cef83d57e2d4067f Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Tue, 7 Oct 2025 11:09:26 -0400 Subject: [PATCH 02/19] Use spinup flowlines for calibration and simulation (#134) Closes #132. Enable use of spinup flowlines in calibration and simulation. --- pygem/__init__.py | 2 +- pygem/bin/op/compress_gdirs.py | 2 +- pygem/bin/op/duplicate_gdirs.py | 2 +- pygem/bin/op/initialize.py | 2 +- pygem/bin/op/list_failed_simulations.py | 2 +- .../postproc/postproc_binned_monthly_mass.py | 2 +- .../postproc/postproc_compile_simulations.py | 2 +- pygem/bin/postproc/postproc_distribute_ice.py | 2 +- pygem/bin/postproc/postproc_monthly_mass.py | 2 +- pygem/bin/preproc/preproc_fetch_mbdata.py | 2 +- pygem/bin/preproc/preproc_wgms_estimate_kp.py | 2 +- pygem/bin/run/__init__.py | 2 +- pygem/bin/run/run_calibration.py | 24 +- .../run/run_calibration_frontalablation.py | 6 +- pygem/bin/run/run_calibration_reg_glena.py | 6 +- pygem/bin/run/run_inversion.py | 266 ++++++++++++------ pygem/bin/run/run_simulation.py | 130 +++++---- pygem/bin/run/run_spinup.py | 90 ++++-- pygem/class_climate.py | 2 +- pygem/gcmbiasadj.py | 2 +- pygem/glacierdynamics.py | 2 +- pygem/massbalance.py | 2 +- pygem/mcmc.py | 2 +- pygem/oggm_compat.py | 37 ++- pygem/output.py | 2 +- pygem/pygem_modelsetup.py | 2 +- pygem/setup/__init__.py | 2 +- pygem/setup/config.py | 10 +- pygem/setup/config.yaml | 4 +- pygem/shop/debris.py | 2 +- pygem/shop/icethickness.py | 2 +- pygem/shop/mbdata.py | 2 +- pygem/shop/oib.py | 2 +- pygem/tests/__init__.py | 2 +- pygem/tests/test_03_notebooks.py | 1 + pygem/utils/_funcs.py | 22 +- pygem/utils/_funcs_selectglaciers.py | 2 +- pygem/utils/stats.py | 2 +- 38 files changed, 429 insertions(+), 221 deletions(-) diff --git a/pygem/__init__.py b/pygem/__init__.py index 65c2ce17..725509ee 100755 --- a/pygem/__init__.py +++ b/pygem/__init__.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license compress OGGM glacier directories """ diff --git a/pygem/bin/op/duplicate_gdirs.py b/pygem/bin/op/duplicate_gdirs.py index af236931..1a3e2bb1 100644 --- a/pygem/bin/op/duplicate_gdirs.py +++ b/pygem/bin/op/duplicate_gdirs.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license duplicate OGGM glacier directories """ diff --git a/pygem/bin/op/initialize.py b/pygem/bin/op/initialize.py index c6f052e1..614a7447 100644 --- a/pygem/bin/op/initialize.py +++ b/pygem/bin/op/initialize.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license initialization script (ensure config.yaml and get sample datasets) """ diff --git a/pygem/bin/op/list_failed_simulations.py b/pygem/bin/op/list_failed_simulations.py index 29e98b22..757cb103 100644 --- a/pygem/bin/op/list_failed_simulations.py +++ b/pygem/bin/op/list_failed_simulations.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license 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 """ diff --git a/pygem/bin/postproc/postproc_binned_monthly_mass.py b/pygem/bin/postproc/postproc_binned_monthly_mass.py index cf57db07..b123bced 100644 --- a/pygem/bin/postproc/postproc_binned_monthly_mass.py +++ b/pygem/bin/postproc/postproc_binned_monthly_mass.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license derive binned monthly ice thickness and mass from PyGEM simulation """ diff --git a/pygem/bin/postproc/postproc_compile_simulations.py b/pygem/bin/postproc/postproc_compile_simulations.py index 0de32b39..a9665f87 100644 --- a/pygem/bin/postproc/postproc_compile_simulations.py +++ b/pygem/bin/postproc/postproc_compile_simulations.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license compile individual glacier simulations to the regional level """ diff --git a/pygem/bin/postproc/postproc_distribute_ice.py b/pygem/bin/postproc/postproc_distribute_ice.py index cfbab6fc..ec909397 100644 --- a/pygem/bin/postproc/postproc_distribute_ice.py +++ b/pygem/bin/postproc/postproc_distribute_ice.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ # Built-in libraries diff --git a/pygem/bin/postproc/postproc_monthly_mass.py b/pygem/bin/postproc/postproc_monthly_mass.py index 267da544..d3a3678f 100644 --- a/pygem/bin/postproc/postproc_monthly_mass.py +++ b/pygem/bin/postproc/postproc_monthly_mass.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license derive monthly glacierwide mass for PyGEM simulation using annual glacier mass and monthly total mass balance """ diff --git a/pygem/bin/preproc/preproc_fetch_mbdata.py b/pygem/bin/preproc/preproc_fetch_mbdata.py index 70d1f2b5..a10063a7 100644 --- a/pygem/bin/preproc/preproc_fetch_mbdata.py +++ b/pygem/bin/preproc/preproc_fetch_mbdata.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Fetch filled Hugonnet reference mass balance data """ diff --git a/pygem/bin/preproc/preproc_wgms_estimate_kp.py b/pygem/bin/preproc/preproc_wgms_estimate_kp.py index 841cdbf3..f9047452 100644 --- a/pygem/bin/preproc/preproc_wgms_estimate_kp.py +++ b/pygem/bin/preproc/preproc_wgms_estimate_kp.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Process the WGMS data to connect with RGIIds and evaluate potential precipitation biases diff --git a/pygem/bin/run/__init__.py b/pygem/bin/run/__init__.py index e7b24f6e..fefe01a7 100755 --- a/pygem/bin/run/__init__.py +++ b/pygem/bin/run/__init__.py @@ -3,5 +3,5 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Run model calibration """ @@ -37,15 +37,10 @@ # read the config pygem_prms = config_manager.read_config() +import pygem.oggm_compat as oggm_compat import pygem.pygem_modelsetup as modelsetup from pygem import class_climate, mcmc from pygem.massbalance import PyGEMMassBalance - -# from pygem.glacierdynamics import MassRedistributionCurveModel -from pygem.oggm_compat import ( - single_flowline_glacier_directory, - single_flowline_glacier_directory_with_calving, -) from pygem.utils.stats import mcmc_stats # from oggm.core import climate @@ -175,6 +170,11 @@ def getparser(): action='store_true', help='Flag to keep glacier lists ordered (default is false)', ) + parser.add_argument( + '-spinup', + action='store_true', + help='Flag to use spinup flowlines (default is false)', + ) parser.add_argument( '-p', '--progress_bar', action='store_true', help='Flag to show progress bar' ) @@ -605,11 +605,13 @@ def run(list_packed_vars): glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation'] ): - gdir = single_flowline_glacier_directory(glacier_str) + gdir = oggm_compat.single_flowline_glacier_directory(glacier_str) gdir.is_tidewater = False else: # set reset=True to overwrite non-calving directory that may already exist - gdir = single_flowline_glacier_directory_with_calving(glacier_str) + gdir = oggm_compat.single_flowline_glacier_directory_with_calving( + glacier_str + ) gdir.is_tidewater = True fls = gdir.read_pickle('inversion_flowlines') @@ -704,6 +706,10 @@ def run(list_packed_vars): 'Mass balance data missing. Check dataset and column names' ) + # if spinup, grab appropriate flowlines + if args.spinup: + fls = oggm_compat.get_spinup_flowlines(gdir, y0=args.ref_startyear) + # ----- CALIBRATION OPTIONS ------ if (fls is not None) and (gdir.mbdata is not None) and (glacier_area.sum() > 0): modelprms = { diff --git a/pygem/bin/run/run_calibration_frontalablation.py b/pygem/bin/run/run_calibration_frontalablation.py index 9523174b..efe2084b 100644 --- a/pygem/bin/run/run_calibration_frontalablation.py +++ b/pygem/bin/run/run_calibration_frontalablation.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Calibrate frontal ablation parameters for tidewater glaciers """ @@ -260,10 +260,10 @@ def reg_calving_flux( cfg.PARAMS['calving_k'] = calving_k cfg.PARAMS['inversion_calving_k'] = cfg.PARAMS['calving_k'] - if pygem_prms['sim']['oggm_dynamics']['use_reg_glena']: + if pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a']: glena_df = pd.read_csv( pygem_prms['root'] - + pygem_prms['sim']['oggm_dynamics']['glena_reg_relpath'] + + pygem_prms['sim']['oggm_dynamics']['glen_a_regional_relpath'] ) glena_idx = np.where(glena_df.O1Region == glacier_rgi_table.O1Region)[ 0 diff --git a/pygem/bin/run/run_calibration_reg_glena.py b/pygem/bin/run/run_calibration_reg_glena.py index e73963b9..5efd9dae 100644 --- a/pygem/bin/run/run_calibration_reg_glena.py +++ b/pygem/bin/run/run_calibration_reg_glena.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Find the optimal values of glens_a_multiplier to match the consensus ice thickness estimates """ @@ -559,7 +559,7 @@ def to_minimize(a_multiplier): try: glena_df = pd.read_csv( - f'{pygem_prms["root"]}/{pygem_prms["out"]["glena_reg_relpath"]}' + f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}' ) # Add or overwrite existing file @@ -578,7 +578,7 @@ def to_minimize(a_multiplier): glena_df = glena_df.sort_values('O1Region', ascending=True) glena_df.reset_index(inplace=True, drop=True) glena_df.to_csv( - f'{pygem_prms["root"]}/{pygem_prms["out"]["glena_reg_relpath"]}', + f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}', index=False, ) diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index d118ad5a..323224fc 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -1,4 +1,5 @@ import argparse +import json import os from functools import partial @@ -21,6 +22,7 @@ # from pygem.glacierdynamics import MassRedistributionCurveModel from pygem.oggm_compat import update_cfg from pygem.shop import debris, mbdata +from pygem.utils._funcs import str2bool cfg.initialize() cfg.PATHS['working_dir'] = ( @@ -28,28 +30,35 @@ ) -def run(glac_no, ncores=1, debug=False): +def run( + glac_no, ncores=1, calibrate_regional_glen_a=False, reset_gdirs=False, debug=False +): """ Run OGGM's bed inversion for a list of RGI glacier IDs using PyGEM's mass balance model. """ - update_cfg({'continue_on_error': True}, 'PARAMS') + update_cfg({'continue_on_error': False}, 'PARAMS') if ncores > 1: update_cfg({'use_multiprocessing': True}, 'PARAMS') update_cfg({'mp_processes': ncores}, 'PARAMS') + if not isinstance(glac_no, list): + glac_no = [glac_no] main_glac_rgi = modelsetup.selectglaciersrgitable(glac_no=glac_no) # get list of RGIId's for each rgitable being run rgiids = main_glac_rgi['RGIId'].tolist() # initialize glacier directories - gdirs = workflow.init_glacier_directories( - rgiids, - from_prepro_level=2, - prepro_border=cfg.PARAMS['border'], - prepro_base_url=pygem_prms['oggm']['base_url'], - prepro_rgi_version='62', - ) + if reset_gdirs: + gdirs = workflow.init_glacier_directories( + rgiids, + from_prepro_level=2, + prepro_border=cfg.PARAMS['border'], + prepro_base_url=pygem_prms['oggm']['base_url'], + prepro_rgi_version='62', + ) + else: + gdirs = workflow.init_glacier_directories(rgiids) # PyGEM setup - model datestable, climate data import, prior model parameters # model dates @@ -134,12 +143,10 @@ def run(glac_no, ncores=1, debug=False): overwrite_gdir=True, ) - ############################## - ### INVERSION - no calving ### - ############################## - if debug: - print('Running initial inversion') - # note, PyGEMMassBalance_wrapper is passed to `tasks.apparent_mb_from_any_mb` as the `mb_model_class` so that PyGEMs mb model is used for inversion + ####################################### + ### CALCULATE APPARENT MASS BALANCE ### + ####################################### + # note, PyGEMMassBalance_wrapper is passed to `tasks.apparent_mb_from_any_mb` as the `mb_model_class` so that PyGEMs mb model is used for apparent mb workflow.execute_entity_task( tasks.apparent_mb_from_any_mb, gdirs, @@ -147,7 +154,6 @@ def run(glac_no, ncores=1, debug=False): PyGEMMassBalance_wrapper, fl_str='inversion_flowlines', option_areaconstant=True, - inversion_filter=True, ), ) # add debris data to flowlines @@ -159,69 +165,112 @@ def run(glac_no, ncores=1, debug=False): ### CALIBRATE GLEN'S A ### ########################## # fit ice thickness to consensus estimates to find "best" Glen's A - if debug: - print("Calibrating Glen's A") - workflow.calibrate_inversion_from_consensus( - gdirs, - apply_fs_on_mismatch=True, - error_on_mismatch=False, # if you running many glaciers some might not work - filter_inversion_output=True, # this partly filters the overdeepening due to - # the equilibrium assumption for retreating glaciers (see. Figure 5 of Maussion et al. 2019) - volume_m3_reference=None, # here you could provide your own total volume estimate in m3 - ) + # note, this runs task.mass_conservation_inversion internally + if calibrate_regional_glen_a: + if debug: + print("Calibrating Glen's A") + workflow.calibrate_inversion_from_consensus( + gdirs, + apply_fs_on_mismatch=True, + error_on_mismatch=False, # if you running many glaciers some might not work + filter_inversion_output=True, # this partly filters the overdeepening due to + # the equilibrium assumption for retreating glaciers (see. Figure 5 of Maussion et al. 2019) + volume_m3_reference=None, # here you could provide your own total volume estimate in m3 + ) - ################################ - ### INVERSION - with calving ### - ################################ - cfg.PARAMS['use_kcalving_for_inversion'] = True for gdir in gdirs: - # note, tidewater glacier inversion is not done in parallel since there's not currently a way to pass a different inversion_calving_k to each gdir + if calibrate_regional_glen_a: + glen_a = gdir.get_diagnostics()['inversion_glen_a'] + fs = gdir.get_diagnostics()['inversion_fs'] + else: + # get glen_a and fs values from prior calibration or manual entry + if pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a']: + glen_a_df = pd.read_csv( + f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}' + ) + glen_a_O1regions = [int(x) for x in glen_a_df.O1Region.values] + assert gdir.glacier_rgi_table.O1Region in glen_a_O1regions, ( + '{0:0.5f}'.format(gd.glacier_rgi_table['RGIId_float']) + + ' O1 region not in glen_a_df' + ) + glen_a_idx = np.where( + glen_a_O1regions == gdir.glacier_rgi_table.O1Region + )[0][0] + glen_a_multiplier = glen_a_df.loc[glen_a_idx, 'glens_a_multiplier'] + fs = glen_a_df.loc[glen_a_idx, 'fs'] + else: + glen_a_multiplier = pygem_prms['sim']['oggm_dynamics'][ + 'glen_a_multiplier' + ] + fs = pygem_prms['sim']['oggm_dynamics']['fs'] + glen_a = cfg.PARAMS['glen_a'] * glen_a_multiplier + + # non-tidewater if ( gdir.glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation'] ): - continue - if debug: - print(f'Running inversion for {gdir.rgi_id} with calving') - - # Load quality controlled frontal ablation data - fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' - assert os.path.exists(fp), 'Calibrated calving dataset does not exist' - calving_df = pd.read_csv(fp) - calving_rgiids = list(calving_df.RGIId) - - # Use calibrated value if individual data available - if gdir.rgi_id in calving_rgiids: - calving_idx = calving_rgiids.index(gdir.rgi_id) - calving_k = calving_df.loc[calving_idx, 'calving_k'] - # Otherwise, use region's median value - else: - calving_df['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values - ] - calving_df_reg = calving_df.loc[ - calving_df['O1Region'] == int(gdir.rgi_id[6:8]), : - ] - calving_k = np.median(calving_df_reg.calving_k) - - # increase calving line for inversion so that later spinup will work - cfg.PARAMS['calving_line_extension'] = 120 - # set inversion_calving_k - cfg.PARAMS['inversion_calving_k'] = calving_k - if debug: - print(f'inversion_calving_k = {calving_k}') + if calibrate_regional_glen_a: + # nothing else to do here - already ran inversion when calibrating Glen's A + continue + + # run inversion using regionally calibrated Glen's A values + cfg.PARAMS['use_kcalving_for_inversion'] = False + + ############################### + ### INVERSION - no calving ### + ################################ + tasks.prepare_for_inversion(gdir) + tasks.mass_conservation_inversion( + gdir, + glen_a=glen_a, + fs=fs, + ) - tasks.find_inversion_calving_from_any_mb( - gdir, - mb_model=PyGEMMassBalance_wrapper( + # tidewater + else: + cfg.PARAMS['use_kcalving_for_inversion'] = True + + # Load quality controlled frontal ablation data + fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' + assert os.path.exists(fp), 'Calibrated calving dataset does not exist' + calving_df = pd.read_csv(fp) + calving_rgiids = list(calving_df.RGIId) + + # Use calibrated value if individual data available + if gdir.rgi_id in calving_rgiids: + calving_idx = calving_rgiids.index(gdir.rgi_id) + calving_k = calving_df.loc[calving_idx, 'calving_k'] + # Otherwise, use region's median value + else: + calving_df['O1Region'] = [ + int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values + ] + calving_df_reg = calving_df.loc[ + calving_df['O1Region'] == int(gdir.rgi_id[6:8]), : + ] + calving_k = np.median(calving_df_reg.calving_k) + + # increase calving line for inversion so that later spinup will work + cfg.PARAMS['calving_line_extension'] = 120 + # set inversion_calving_k + cfg.PARAMS['inversion_calving_k'] = calving_k + if debug: + print(f'inversion_calving_k = {calving_k}') + + ################################ + ### INVERSION - with calving ### + ################################ + tasks.find_inversion_calving_from_any_mb( gdir, - fl_str='inversion_flowlines', - option_areaconstant=True, - inversion_filter=True, - ), - glen_a=gdir.get_diagnostics()['inversion_glen_a'], - fs=gdir.get_diagnostics()['inversion_fs'], - ) + mb_model=PyGEMMassBalance_wrapper( + gdir, + fl_str='inversion_flowlines', + option_areaconstant=True, + ), + glen_a=glen_a, + fs=fs, + ) ###################### ### POSTPROCESSING ### @@ -236,7 +285,7 @@ def run(glac_no, ncores=1, debug=False): def main(): # define ArgumentParser parser = argparse.ArgumentParser( - description="perform glacier bed inversion (defaults to find best Glen's A for each RGI order 01 region)" + description="Perform glacier bed inversion (defaults to find best Glen's A for each RGI order 01 region)" ) # add arguments parser.add_argument( @@ -246,31 +295,76 @@ def main(): help='Randoph Glacier Inventory region (can take multiple, e.g. `-run_region01 1 2 3`)', nargs='+', ) + parser.add_argument( + '-rgi_glac_number', + action='store', + type=float, + default=pygem_prms['setup']['glac_no'], + nargs='+', + help='Randoph Glacier Inventory glacier number (can take multiple)', + ) + ( + parser.add_argument( + '-rgi_glac_number_fn', + action='store', + type=str, + default=None, + help='Filepath containing list of rgi_glac_number, helpful for running batches on spc', + ), + ) + parser.add_argument( + '-calibrate_regional_glen_a', + type=str2bool, + default=True, + help="If True (False) run ice thickness inversion and regionally calibrate (use previously calibrated or user-input) Glen's A values. Default is True", + ) parser.add_argument( '-ncores', action='store', type=int, default=1, - help='number of simultaneous processes (cores) to use', + help='Number of simultaneous processes (cores) to use', + ) + parser.add_argument( + '-reset_gdirs', + action='store_true', + help='If True (False) reset OGGM glacier directories. Default is False', ) parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') args = parser.parse_args() - # RGI glacier number - batches = [ - modelsetup.selectglaciersrgitable( - rgi_regionsO1=[r01], - rgi_regionsO2='all', - include_landterm=pygem_prms['setup']['include_landterm'], - include_laketerm=pygem_prms['setup']['include_laketerm'], - include_tidewater=pygem_prms['setup']['include_tidewater'], - min_glac_area_km2=pygem_prms['setup']['min_glac_area_km2'], - )['rgino_str'].values.tolist() - for r01 in args.rgi_region01 - ] + # RGI glacier batches + if args.rgi_region01: + batches = [ + modelsetup.selectglaciersrgitable( + rgi_regionsO1=[r01], + rgi_regionsO2='all', + include_landterm=pygem_prms['setup']['include_landterm'], + include_laketerm=pygem_prms['setup']['include_laketerm'], + include_tidewater=pygem_prms['setup']['include_tidewater'], + min_glac_area_km2=pygem_prms['setup']['min_glac_area_km2'], + )['rgino_str'].values.tolist() + for r01 in args.rgi_region01 + ] + else: + batches = None + if args.rgi_glac_number: + glac_no = args.rgi_glac_number + # format appropriately + glac_no = [float(g) for g in glac_no] + batches = [f'{g:.5f}' if g >= 10 else f'0{g:.5f}' for g in glac_no] + elif args.rgi_glac_number_fn is not None: + with open(args.rgi_glac_number_fn, 'r') as f: + batches = json.load(f) # set up partial function with common arguments - run_partial = partial(run, ncores=args.ncores, debug=args.debug) + run_partial = partial( + run, + ncores=args.ncores, + calibrate_regional_glen_a=args.calibrate_regional_glen_a, + reset_gdirs=args.reset_gdirs, + debug=args.debug, + ) for i, batch in enumerate(batches): run_partial(batch) diff --git a/pygem/bin/run/run_simulation.py b/pygem/bin/run/run_simulation.py index 7bf357d2..0f365d7c 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Run a model simulation """ @@ -269,10 +269,10 @@ def getparser(): help='glacier dynamics scheme (options: ``OGGM`, `MassRedistributionCurves`, `None`)', ) parser.add_argument( - '-use_reg_glena', + '-use_regional_glen_a', action='store', type=bool, - default=pygem_prms['sim']['oggm_dynamics']['use_reg_glena'], + default=pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a'], help='Take the glacier flow parameterization from regionally calibrated priors (boolean: `0` or `1`, `True` or `False`)', ) parser.add_argument( @@ -335,6 +335,12 @@ def getparser(): action='store_true', help='Flag to keep glacier lists ordered (default is off)', ) + parser.add_argument( + '-spinup', + action='store_true', + default=False, + help='Flag to perform dynamical spinup before calibration', + ) parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') return parser @@ -839,9 +845,9 @@ def run(list_packed_vars): if debug: print('cfl number:', cfg.PARAMS['cfl_number']) - if args.use_reg_glena: + if args.use_regional_glen_a: glena_df = pd.read_csv( - f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glena_reg_relpath"]}' + f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}' ) glena_O1regions = [int(x) for x in glena_df.O1Region.values] assert glacier_rgi_table.O1Region in glena_O1regions, ( @@ -860,6 +866,20 @@ def run(list_packed_vars): glen_a_multiplier = pygem_prms['sim']['oggm_dynamics'][ 'glen_a_multiplier' ] + glen_a = cfg.PARAMS['glen_a'] * glen_a_multiplier + + # spinup + if args.spinup: + try: + # see if model_flowlines from spinup exist + nfls = gdir.read_pickle( + 'model_flowlines', + filesuffix=f'_dynamic_spinup_pygem_mb_{args.sim_startyear}', + ) + except: + raise + glen_a = gdir.get_diagnostics()['inversion_glen_a'] + fs = gdir.get_diagnostics()['inversion_fs'] # Time attributes and values if pygem_prms['climate']['sim_wateryear'] == 'hydro': @@ -978,65 +998,59 @@ def run(list_packed_vars): else: inversion_filter = False - # Perform inversion based on PyGEM MB using reference directory - mbmod_inv = PyGEMMassBalance( - gdir_ref, - modelprms, - glacier_rgi_table, - fls=fls, - option_areaconstant=True, - inversion_filter=inversion_filter, - ) - # if debug: - # h, w = gdir.get_inversion_flowline_hw() - # mb_t0 = (mbmod_inv.get_annual_mb(h, year=0, fl_id=0, fls=fls) * cfg.SEC_IN_YEAR * - # pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water']) - # plt.plot(mb_t0, h, '.') - # plt.ylabel('Elevation') - # plt.xlabel('Mass balance (mwea)') - # plt.show() - - # Non-tidewater glaciers - if ( - not gdir.is_tidewater - or not pygem_prms['setup']['include_frontalablation'] - ): - # Arbitrariliy shift the MB profile up (or down) until mass balance is zero (equilibrium for inversion) - apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) - tasks.prepare_for_inversion(gdir) - tasks.mass_conservation_inversion( - gdir, - glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, - fs=fs, + # run inversion + if not args.spinup: + # Perform inversion based on PyGEM MB using reference directory + mbmod_inv = PyGEMMassBalance( + gdir_ref, + modelprms, + glacier_rgi_table, + fls=fls, + option_areaconstant=True, + inversion_filter=inversion_filter, ) - # Tidewater glaciers - else: - cfg.PARAMS['use_kcalving_for_inversion'] = True - cfg.PARAMS['use_kcalving_for_run'] = True - tasks.find_inversion_calving_from_any_mb( - gdir, - mb_model=mbmod_inv, - glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, - fs=fs, - ) + # Non-tidewater glaciers + if ( + not gdir.is_tidewater + or not pygem_prms['setup']['include_frontalablation'] + ): + # Arbitrariliy shift the MB profile up (or down) until mass balance is zero (equilibrium for inversion) + apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) + tasks.prepare_for_inversion(gdir) + tasks.mass_conservation_inversion( + gdir, + glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, + fs=fs, + ) - # ----- INDENTED TO BE JUST WITH DYNAMICS ----- - tasks.init_present_time_glacier(gdir) # adds bins below + # Tidewater glaciers + else: + cfg.PARAMS['use_kcalving_for_inversion'] = True + cfg.PARAMS['use_kcalving_for_run'] = True + tasks.find_inversion_calving_from_any_mb( + gdir, + mb_model=mbmod_inv, + glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, + fs=fs, + ) - if not os.path.isfile(gdir.get_filepath('model_flowlines')): - tasks.compute_downstream_line(gdir) - tasks.compute_downstream_bedshape(gdir) + # ----- INDENTED TO BE JUST WITH DYNAMICS ----- tasks.init_present_time_glacier(gdir) # adds bins below - try: - if pygem_prms['mb']['include_debris']: - debris.debris_binned( - gdir, fl_str='model_flowlines' - ) # add debris enhancement factors to flowlines - nfls = gdir.read_pickle('model_flowlines') - except: - raise + if not os.path.isfile(gdir.get_filepath('model_flowlines')): + tasks.compute_downstream_line(gdir) + tasks.compute_downstream_bedshape(gdir) + tasks.init_present_time_glacier(gdir) # adds bins below + + try: + if pygem_prms['mb']['include_debris']: + debris.debris_binned( + gdir, fl_str='model_flowlines' + ) # add debris enhancement factors to flowlines + nfls = gdir.read_pickle('model_flowlines') + except: + raise # Water Level # Check that water level is within given bounds diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index 2ae7eb24..102e93a8 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -27,12 +27,13 @@ ) -def run(glacno_list, spinup_start_yr, **kwargs): +def run(glacno_list, mb_model_params, debug=False, **kwargs): + # remove None kwargs + kwargs = {k: v for k, v in kwargs.items() if v is not None} + main_glac_rgi = modelsetup.selectglaciersrgitable(glac_no=glacno_list) # model dates - dt = modelsetup.datesmodelrun( - startyear=spinup_start_yr, endyear=2019 - ) # will have to cover the time period of inversion (2000-2019) and spinup (1979-~2010 by default) + dt = modelsetup.datesmodelrun(startyear=1979, endyear=2019) # load climate data ref_clim = class_climate.GCM(name='ERA5') @@ -91,22 +92,44 @@ def run(glacno_list, spinup_start_yr, **kwargs): } gd.dates_table = dt - # get modelprms from regional priors - priors_idx = np.where( - (priors_df.O1Region == gd.glacier_rgi_table['O1Region']) - & (priors_df.O2Region == gd.glacier_rgi_table['O2Region']) - )[0][0] - tbias_mu = float(priors_df.loc[priors_idx, 'tbias_mean']) - kp_mu = float(priors_df.loc[priors_idx, 'kp_mean']) - gd.modelprms = { - 'kp': kp_mu, - 'tbias': tbias_mu, - 'ddfsnow': pygem_prms['calib']['MCMC_params']['ddfsnow_mu'], - 'ddfice': pygem_prms['calib']['MCMC_params']['ddfsnow_mu'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'], - 'precgrad': pygem_prms['sim']['params']['precgrad'], - 'tsnow_threshold': pygem_prms['sim']['params']['tsnow_threshold'], - } + if mb_model_params == 'regional_priors': + # get modelprms from regional priors + priors_idx = np.where( + (priors_df.O1Region == gd.glacier_rgi_table['O1Region']) + & (priors_df.O2Region == gd.glacier_rgi_table['O2Region']) + )[0][0] + tbias_mu = float(priors_df.loc[priors_idx, 'tbias_mean']) + kp_mu = float(priors_df.loc[priors_idx, 'kp_mean']) + gd.modelprms = { + 'kp': kp_mu, + 'tbias': tbias_mu, + 'ddfsnow': pygem_prms['calib']['MCMC_params']['ddfsnow_mu'], + 'ddfice': pygem_prms['calib']['MCMC_params']['ddfsnow_mu'] + / pygem_prms['sim']['params']['ddfsnow_iceratio'], + 'precgrad': pygem_prms['sim']['params']['precgrad'], + 'tsnow_threshold': pygem_prms['sim']['params']['tsnow_threshold'], + } + elif mb_model_params == 'emulator': + # get modelprms from emulator mass balance calibration + modelprms_fn = glacier_str + '-modelprms_dict.json' + modelprms_fp = ( + pygem_prms['root'] + + '/Output/calibration/' + + glacier_str.split('.')[0].zfill(2) + + '/' + ) + modelprms_fn + with open(modelprms_fp, 'r') as f: + modelprms_dict = json.load(f) + + modelprms_all = modelprms_dict['emulator'] + gd.modelprms = { + 'kp': modelprms_all['kp'][0], + 'tbias': modelprms_all['tbias'][0], + 'ddfsnow': modelprms_all['ddfsnow'][0], + 'ddfice': modelprms_all['ddfice'][0], + 'tsnow_threshold': modelprms_all['tsnow_threshold'][0], + 'precgrad': modelprms_all['precgrad'][0], + } # update cfg.PARAMS update_cfg({'continue_on_error': True}, 'PARAMS') @@ -116,10 +139,10 @@ def run(glacno_list, spinup_start_yr, **kwargs): workflow.execute_entity_task( tasks.run_dynamic_spinup, gd, - spinup_start_yr=spinup_start_yr, # When to start the spinup + # spinup_start_yr=spinup_start_yr, # When to start the spinup minimise_for='area', # what target to match at the RGI date # target_yr=target_yr, # The year at which we want to match area or volume. If None, gdir.rgi_date + 1 is used (the default) - # ye=, # When the simulation should stop + # ye=ye, # When the simulation should stop output_filesuffix='_dynamic_spinup_pygem_mb', store_fl_diagnostics=True, store_model_geometry=True, @@ -169,10 +192,15 @@ def main(): action='store', type=str, default=None, - help='filepath containing list of rgi_glac_number, helpful for running batches on spc', + help='Filepath containing list of rgi_glac_number, helpful for running batches on spc', ), ) - parser.add_argument('-spinup_start_yr', type=int, default=1979) + parser.add_argument( + '-spinup_period', + type=int, + default=None, + help='Fixed spinup period (years). If not provided, OGGM default is used.', + ) parser.add_argument('-target_yr', type=int, default=None) parser.add_argument('-ye', type=int, default=2020) parser.add_argument( @@ -180,7 +208,14 @@ def main(): action='store', type=int, default=1, - help='number of simultaneous processes (cores) to use', + help='Number of simultaneous processes (cores) to use', + ) + parser.add_argument( + '-mb_model_params', + type=str, + default='regional_priors', + choices=['regional_priors', 'emulator'], + help='Which mass balance model parameters to use ("regional_priors" or "emulator")', ) args = parser.parse_args() @@ -221,7 +256,10 @@ def main(): # set up partial function with debug argument run_partial = partial( - run, spinup_start_yr=args.spinup_start_yr, target_yr=args.target_yr, ye=args.ye + run, + mb_model_params=args.mb_model_params, + target_yr=args.target_yr, + spinup_period=args.spinup_period, ) # parallel processing print(f'Processing with {ncores} cores... \n{glac_no_lsts}') diff --git a/pygem/class_climate.py b/pygem/class_climate.py index e39e19dd..300d13d5 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Run bias adjustments a given climate dataset """ diff --git a/pygem/glacierdynamics.py b/pygem/glacierdynamics.py index 5ba4b8ad..02b607df 100755 --- a/pygem/glacierdynamics.py +++ b/pygem/glacierdynamics.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ diff --git a/pygem/setup/config.py b/pygem/setup/config.py index 33cdc1d0..86d28094 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ import os @@ -129,8 +129,8 @@ def _validate_config(self, config): 'user.institution': (str, type(None)), 'user.email': (str, type(None)), 'setup': dict, - 'setup.rgi_region01': list, - 'setup.rgi_region02': str, + 'setup.rgi_region01': (list, type(None)), + 'setup.rgi_region02': (str, type(None)), 'setup.glac_no_skip': (list, type(None)), 'setup.glac_no': (list, type(None)), 'setup.min_glac_area_km2': int, @@ -276,8 +276,8 @@ def _validate_config(self, config): 'sim.oggm_dynamics': dict, 'sim.oggm_dynamics.cfl_number': float, 'sim.oggm_dynamics.cfl_number_calving': float, - 'sim.oggm_dynamics.glena_reg_relpath': str, - 'sim.oggm_dynamics.use_reg_glena': bool, + 'sim.oggm_dynamics.glen_a_regional_relpath': str, + 'sim.oggm_dynamics.use_regional_glen_a': bool, 'sim.oggm_dynamics.fs': int, 'sim.oggm_dynamics.glen_a_multiplier': int, 'sim.icethickness_advancethreshold': int, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 57b6ae27..6bb8e578 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -226,8 +226,8 @@ sim: oggm_dynamics: cfl_number: 0.02 # Time step threshold (seconds) cfl_number_calving: 0.01 # Time step threshold for marine-terimating glaciers (seconds) - use_reg_glena: true - glena_reg_relpath: /Output/calibration/glena_region.csv + use_regional_glen_a: true + glen_a_regional_relpath: /Output/calibration/glena_region.csv # glen_a multiplier if not using regionally calibrated glens_a fs: 0 glen_a_multiplier: 1 diff --git a/pygem/shop/debris.py b/pygem/shop/debris.py index 0c4c922b..66c548f4 100755 --- a/pygem/shop/debris.py +++ b/pygem/shop/debris.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ import logging diff --git a/pygem/shop/icethickness.py b/pygem/shop/icethickness.py index 256bf526..a17cfbb4 100755 --- a/pygem/shop/icethickness.py +++ b/pygem/shop/icethickness.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ import logging diff --git a/pygem/shop/mbdata.py b/pygem/shop/mbdata.py index b62dc856..9b23897b 100755 --- a/pygem/shop/mbdata.py +++ b/pygem/shop/mbdata.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ # Built-in libaries diff --git a/pygem/shop/oib.py b/pygem/shop/oib.py index b7be9611..a35ce02d 100644 --- a/pygem/shop/oib.py +++ b/pygem/shop/oib.py @@ -3,7 +3,7 @@ copyright © 2024 Brandon Tober , David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license NASA Operation IceBridge data and processing class """ diff --git a/pygem/tests/__init__.py b/pygem/tests/__init__.py index e7b24f6e..fefe01a7 100755 --- a/pygem/tests/__init__.py +++ b/pygem/tests/__init__.py @@ -3,5 +3,5 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Functions that didn't fit into other modules """ +import argparse import json import numpy as np @@ -20,6 +21,25 @@ pygem_prms = config_manager.read_config() +def str2bool(v): + """ + Convert a string to a boolean. + + Accepts: "yes", "true", "t", "1" → True; + "no", "false", "f", "0" → False. + + Raises an error if input is unrecognized. + """ + if isinstance(v, bool): + return v + if v.lower() in ('yes', 'true', 't', '1', 'y'): + return True + elif v.lower() in ('no', 'false', 'f', '0', 'n'): + return False + else: + raise argparse.ArgumentTypeError('Boolean value expected.') + + def annualweightedmean_array(var, dates_table): """ Calculate annual mean of variable according to the timestep. diff --git a/pygem/utils/_funcs_selectglaciers.py b/pygem/utils/_funcs_selectglaciers.py index 742d9314..86fcff02 100644 --- a/pygem/utils/_funcs_selectglaciers.py +++ b/pygem/utils/_funcs_selectglaciers.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Functions of different ways to select glaciers """ diff --git a/pygem/utils/stats.py b/pygem/utils/stats.py index a09b15a2..8f61f98d 100644 --- a/pygem/utils/stats.py +++ b/pygem/utils/stats.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Model statistics module """ From 5949dd42a8d37e6db4a40ecfc7a551f052c0b71d Mon Sep 17 00:00:00 2001 From: Albin Wells <91861031+albinwwells@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:54:02 -0400 Subject: [PATCH 03/19] Added functionality for importing daily ERA5 data (#136) Closes #136 --- pygem/bin/run/run_calibration.py | 4 +- pygem/class_climate.py | 142 ++++++++++++++++++++++++++----- pygem/pygem_modelsetup.py | 8 +- pygem/setup/config.py | 1 + pygem/setup/config.yaml | 4 +- 5 files changed, 132 insertions(+), 27 deletions(-) diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index f2d623f2..be0f01ed 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -581,9 +581,9 @@ def run(list_packed_vars): gcm_elev = gcm.importGCMfxnearestneighbor_xarray( gcm.elev_fn, gcm.elev_vn, main_glac_rgi ) - # Lapse rate [degC m-1] + # Lapse rate [degC m-1] (always monthly) gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table + gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, upscale_var_timestep=True ) # ===== LOOP THROUGH GLACIERS TO RUN CALIBRATION ===== diff --git a/pygem/class_climate.py b/pygem/class_climate.py index 300d13d5..d2564eff 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -111,7 +111,8 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + pygem_prms['climate']['paths']['cesm2_fp_fx_ending'] ) # Extra information - self.timestep = pygem_prms['time']['timestep'] + # self.timestep = pygem_prms['time']['timestep'] + self.timestep = 'monthly' # future scenario is always monthly timestep self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] self.sim_climate_scenario = sim_climate_scenario @@ -164,7 +165,8 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + pygem_prms['climate']['paths']['gfdl_fp_fx_ending'] ) # Extra information - self.timestep = pygem_prms['time']['timestep'] + # self.timestep = pygem_prms['time']['timestep'] + self.timestep = 'monthly' # future scenario is always monthly timestep self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] self.sim_climate_scenario = sim_climate_scenario @@ -190,12 +192,18 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.elev_fn = pygem_prms['climate']['paths']['era5_elev_fn'] self.lr_fn = pygem_prms['climate']['paths']['era5_lr_fn'] # Variable filepaths - self.var_fp = ( - pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] - ) - self.fx_fp = ( - pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] - ) + if pygem_prms['climate']['paths']['era5_fullpath']: + self.var_fp = '' + self.fx_fp = '' + else: + self.var_fp = ( + pygem_prms['root'] + + pygem_prms['climate']['paths']['era5_relpath'] + ) + self.fx_fp = ( + pygem_prms['root'] + + pygem_prms['climate']['paths']['era5_relpath'] + ) # Extra information self.timestep = pygem_prms['time']['timestep'] self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] @@ -295,7 +303,8 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + '/' ) # Extra information - self.timestep = pygem_prms['time']['timestep'] + # self.timestep = pygem_prms['time']['timestep'] + self.timestep = 'monthly' # future scenario is always monthly timestep self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] self.sim_climate_scenario = sim_climate_scenario @@ -341,7 +350,8 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + '/' ) # Extra information - self.timestep = pygem_prms['time']['timestep'] + # self.timestep = pygem_prms['time']['timestep'] + self.timestep = 'monthly' # future scenario is always monthly timestep self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] self.sim_climate_scenario = sim_climate_scenario @@ -443,6 +453,7 @@ def importGCMvarnearestneighbor_xarray( main_glac_rgi, dates_table, realizations=['r1i1p1f1', 'r4i1p1f1'], + upscale_var_timestep=False, ): """ Import time series of variables and extract nearest neighbor. @@ -472,13 +483,99 @@ def importGCMvarnearestneighbor_xarray( timestep, i.e., be from the beginning/middle/end of month) """ # Import netcdf file - if not os.path.exists(self.var_fp + filename): - if os.path.exists(self.var_fp + filename.replace('r1i1p1f1', 'r4i1p1f1')): - filename = filename.replace('r1i1p1f1', 'r4i1p1f1') - if os.path.exists(self.var_fp + filename.replace('_native', '')): - filename = filename.replace('_native', '') + if self.timestep == 'monthly': + if not os.path.exists(self.var_fp + filename): + if os.path.exists( + self.var_fp + filename.replace('r1i1p1f1', 'r4i1p1f1') + ): + filename = filename.replace('r1i1p1f1', 'r4i1p1f1') + if os.path.exists(self.var_fp + filename.replace('_native', '')): + filename = filename.replace('_native', '') + + data = xr.open_dataset(self.var_fp + filename) + elif self.timestep == 'daily': + year_start = pd.Timestamp(dates_table['date'].values[0]).year + year_end = pd.Timestamp(dates_table['date'].values[-1]).year + + lats = main_glac_rgi[self.rgi_lat_colname].values + lons = main_glac_rgi[self.rgi_lon_colname].values + # define lat/lon window around all glaciers in run (bounds expanded to nearest 0.25 degrees) + min_lat, max_lat = np.floor(lats.min() * 4) / 4, np.ceil(lats.max() * 4) / 4 + min_lon, max_lon = np.floor(lons.min() * 4) / 4, np.ceil(lons.max() * 4) / 4 + if 'YYYY' in filename: + datasets = [] + for yr in range(year_start, year_end + 1): + data_yr = xr.open_dataset( + self.var_fp + filename.replace('YYYY', str(yr)) + ) + if 'valid_time' in data_yr.coords or 'valid_time' in data_yr.dims: + data_yr = data_yr.rename({'valid_time': self.time_vn}) + + # convert longitude from -180—180 to 0—360 + if data_yr.longitude.min() < 0: + data_yr = data_yr.assign_coords( + longitude=(data_yr.longitude % 360) + ) + + # subset for desired lats and lons + data_yr = data_yr.sel( + latitude=slice(max_lat, min_lat), + longitude=slice(min_lon, max_lon), + ) + + # append data to list + datasets.append(data_yr) + + # combine along the time dimension + data = xr.concat(datasets, dim=self.time_vn) + + else: + data = xr.open_dataset(self.var_fp + filename) + data = data.sel( + latitude=slice(max_lat, min_lat), longitude=slice(min_lon, max_lon) + ) + + # mask out leap days + if pygem_prms['time']['option_leapyear'] == 0 and not upscale_var_timestep: + time_index = pd.to_datetime(data[self.time_vn].values) + mask = ~((time_index.month == 2) & (time_index.day == 29)) + data = data.isel({self.time_vn: mask}) + + # Upscale timestep to match data (e.g., monthly lapserate for daily data) + if upscale_var_timestep: + # convert time to datetime + time_monthly = pd.to_datetime(data[self.time_vn].values) + var_monthly = data[vn] # xarray DataArray with dims (time, lat, lon) + + # create empty DataArray for daily data + daily_times = dates_table['date'].values + daily_data = xr.DataArray( + np.zeros( + (len(daily_times), len(data.latitude), len(data.longitude)) + ), + dims=(self.time_vn, 'latitude', 'longitude'), + coords={ + self.time_vn: daily_times, + 'latitude': data.latitude, + 'longitude': data.longitude, + }, + name=vn, + ) + + # loop through months and fill daily slots + for i, t in enumerate(time_monthly): + # find all days in this month + idx = np.where( + (dates_table['year'] == t.year) + & (dates_table['month'] == t.month) + )[0] + + # assign monthly values to these daily indices + daily_data[idx, :, :] = var_monthly.isel(time=i).values + + # convert to Dataset with data variable vn + data = daily_data.to_dataset(name=vn) - data = xr.open_dataset(self.var_fp + filename) glac_variable_series = np.zeros((main_glac_rgi.shape[0], dates_table.shape[0])) # Check GCM provides required years of data @@ -525,7 +622,9 @@ def importGCMvarnearestneighbor_xarray( pd.Series(data[self.time_vn]).apply( lambda x: x.strftime('%Y-%m-%d') ) - == dates_table['date'].apply(lambda x: x.strftime('%Y-%m-%d'))[0] + == dates_table['date'] + .apply(lambda x: x.strftime('%Y-%m-%d')) + .iloc[0] ) )[0][0] end_idx = ( @@ -533,11 +632,12 @@ def importGCMvarnearestneighbor_xarray( pd.Series(data[self.time_vn]).apply( lambda x: x.strftime('%Y-%m-%d') ) - == dates_table['date'].apply(lambda x: x.strftime('%Y-%m-%d'))[ - dates_table.shape[0] - 1 - ] + == dates_table['date'] + .apply(lambda x: x.strftime('%Y-%m-%d')) + .iloc[-1] ) )[0][0] + # Extract the time series time_series = pd.Series(data[self.time_vn][start_idx : end_idx + 1]) # Find Nearest Neighbor @@ -577,6 +677,7 @@ def importGCMvarnearestneighbor_xarray( # Find unique latitude/longitudes latlon_nearidx = list(zip(lat_nearidx, lon_nearidx)) latlon_nearidx_unique = list(set(latlon_nearidx)) + # Create dictionary of time series for each unique latitude/longitude glac_variable_dict = {} for latlon in latlon_nearidx_unique: @@ -634,4 +735,5 @@ def importGCMvarnearestneighbor_xarray( ) elif vn != self.lr_vn: print('Check units of air temperature or precipitation') + return glac_variable_series, time_series diff --git a/pygem/pygem_modelsetup.py b/pygem/pygem_modelsetup.py index 090fa8c6..d47894ec 100755 --- a/pygem/pygem_modelsetup.py +++ b/pygem/pygem_modelsetup.py @@ -100,8 +100,9 @@ def datesmodelrun( dates_table['month'] = dates_table['date'].dt.month dates_table['day'] = dates_table['date'].dt.day dates_table['daysinmonth'] = dates_table['date'].dt.daysinmonth + dates_table['timestep'] = np.arange(len(dates_table['date'])) # Set date as index - dates_table.set_index('date', inplace=True) + dates_table.set_index('timestep', inplace=True) # Remove leap year days if user selected this with option_leapyear if pygem_prms['time']['option_leapyear'] == 0: # First, change 'daysinmonth' number @@ -119,9 +120,8 @@ def datesmodelrun( # Water year for northern hemisphere using USGS definition (October 1 - September 30th), # e.g., water year for 2000 is from October 1, 1999 - September 30, 2000 dates_table['wateryear'] = dates_table['year'] - for step in range(dates_table.shape[0]): - if dates_table.loc[step, 'month'] >= 10: - dates_table.loc[step, 'wateryear'] = dates_table.loc[step, 'year'] + 1 + dates_table.loc[dates_table['month'] >= 10, 'wateryear'] = dates_table['year'] + 1 + # Add column for seasons # create a season dictionary to assist groupby functions seasondict = {} diff --git a/pygem/setup/config.py b/pygem/setup/config.py index 86d28094..608daf7b 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -157,6 +157,7 @@ def _validate_config(self, config): 'climate.sim_wateryear': str, 'climate.constantarea_years': int, 'climate.paths': dict, + 'climate.paths.era5_fullpath': bool, 'climate.paths.era5_relpath': str, 'climate.paths.era5_temp_fn': str, 'climate.paths.era5_tempstd_fn': str, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 6bb8e578..9755acee 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -66,6 +66,7 @@ climate: # ===== CLIMATE DATA FILEPATHS AND FILENAMES ===== paths: # ERA5 (default reference climate data) + era5_fullpath: False # bool. 'True' ignores 'root' and 'era5_relpath' for ERA5 data and assumes below filenames are absolute era5_relpath: /climate_data/ERA5/ era5_temp_fn: ERA5_temp_monthly.nc era5_tempstd_fn: ERA5_tempstd_monthly.nc @@ -73,6 +74,7 @@ climate: era5_elev_fn: ERA5_geopotential.nc era5_pressureleveltemp_fn: ERA5_pressureleveltemp_monthly.nc era5_lr_fn: ERA5_lapserates_monthly.nc + # CMIP5 (GCM data) cmip5_relpath: /climate_data/cmip5/ cmip5_fp_var_ending: _r1i1p1_monNG/ @@ -324,7 +326,7 @@ time: wateryear_month_start: 10 # water year starting month winter_month_start: 10 # first month of winter (for HMA winter is October 1 - April 30) summer_month_start: 5 # first month of summer (for HMA summer is May 1 - Sept 30) - timestep: monthly # time step ('monthly' only option at present) + timestep: monthly # time step ('monthly' or 'daily') # ===== MODEL CONSTANTS ===== constants: From 5f3bacfb36adb505e0cda04cd50fed0deb673f3e Mon Sep 17 00:00:00 2001 From: Albin Wells <91861031+albinwwells@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:29:24 -0400 Subject: [PATCH 04/19] Issue 128 -- importing melt extent and snowline data into OGGM gdirs (#139) Closes #128 --- .../sample_melt_extent_elev.csv | 336 ++++++++++++++++++ .../sample_snowline_elev.csv | 336 ++++++++++++++++++ pygem/bin/run/run_calibration.py | 13 +- .../run/run_calibration_frontalablation.py | 8 +- pygem/bin/run/run_calibration_reg_glena.py | 12 +- pygem/bin/run/run_inversion.py | 6 +- pygem/bin/run/run_simulation.py | 34 +- pygem/bin/run/run_spinup.py | 6 +- pygem/class_climate.py | 43 ++- pygem/oggm_compat.py | 24 +- pygem/setup/config.py | 7 +- pygem/setup/config.yaml | 10 +- pygem/shop/meltextent_and_snowline_1d.py | 289 +++++++++++++++ pyproject.toml | 3 +- 14 files changed, 1085 insertions(+), 42 deletions(-) create mode 100644 docs/_static/example_meltextent_and_snowline_1d/sample_melt_extent_elev.csv create mode 100644 docs/_static/example_meltextent_and_snowline_1d/sample_snowline_elev.csv create mode 100644 pygem/shop/meltextent_and_snowline_1d.py diff --git a/docs/_static/example_meltextent_and_snowline_1d/sample_melt_extent_elev.csv b/docs/_static/example_meltextent_and_snowline_1d/sample_melt_extent_elev.csv new file mode 100644 index 00000000..62bc29de --- /dev/null +++ b/docs/_static/example_meltextent_and_snowline_1d/sample_melt_extent_elev.csv @@ -0,0 +1,336 @@ +date,z,z_min,z_max,direction,ref_dem_year +2016-11-24,1229.0,1182.0,1260.0,ascending,2013 +2016-12-18,1182.0,1182.0,1186.0,ascending,2013 +2017-01-11,1182.0,1182.0,1182.0,ascending,2013 +2017-02-04,1182.0,1182.0,1182.0,ascending,2013 +2017-02-28,1182.0,1182.0,1182.0,ascending,2013 +2017-03-12,1182.0,1182.0,1182.0,ascending,2013 +2017-03-24,1182.0,1182.0,1182.0,ascending,2013 +2017-04-05,1182.0,1182.0,1182.0,ascending,2013 +2017-04-17,1239.0,1209.0,1265.0,ascending,2013 +2017-04-29,1543.0,1497.0,1579.0,ascending,2013 +2017-05-11,1915.0,1864.0,1966.0,ascending,2013 +2017-05-23,1844.0,1812.0,1878.0,ascending,2013 +2017-06-04,2312.0,2273.0,2387.0,ascending,2013 +2017-06-16,2324.0,2307.0,2371.0,ascending,2013 +2017-06-28,2329.0,2314.0,2371.0,ascending,2013 +2017-07-10,2360.0,2326.0,2371.0,ascending,2013 +2017-07-22,2340.0,2318.0,2371.0,ascending,2013 +2017-08-15,2335.0,2315.0,2387.0,ascending,2013 +2017-08-27,2346.0,2318.0,2387.0,ascending,2013 +2017-09-08,2364.0,2346.0,2385.0,ascending,2013 +2017-09-20,2288.0,2239.0,2360.0,ascending,2013 +2017-10-02,2215.0,2155.0,2303.0,ascending,2013 +2017-11-07,1428.0,1186.0,1625.0,ascending,2013 +2017-12-01,1182.0,1182.0,1182.0,ascending,2013 +2017-12-13,1182.0,1182.0,1182.0,ascending,2013 +2018-01-06,1182.0,1182.0,1182.0,ascending,2013 +2018-01-18,1182.0,1182.0,1182.0,ascending,2013 +2018-02-11,1182.0,1182.0,1182.0,ascending,2013 +2018-02-23,1182.0,1182.0,1182.0,ascending,2013 +2018-03-07,1182.0,1182.0,1182.0,ascending,2013 +2018-03-19,1182.0,1182.0,1182.0,ascending,2013 +2018-03-31,1182.0,1182.0,1182.0,ascending,2013 +2018-04-12,1304.0,1284.0,1327.0,ascending,2013 +2018-04-24,1365.0,1353.0,1374.0,ascending,2013 +2018-05-06,1199.0,1186.0,1211.0,ascending,2013 +2018-05-18,1729.0,1704.0,1757.0,ascending,2013 +2018-05-30,2184.0,2139.0,2231.0,ascending,2013 +2018-06-11,2385.0,2371.0,2387.0,ascending,2013 +2018-06-23,2385.0,2371.0,2387.0,ascending,2013 +2018-07-05,2369.0,2360.0,2387.0,ascending,2013 +2018-07-17,2371.0,2369.0,2387.0,ascending,2013 +2018-07-29,2333.0,2314.0,2387.0,ascending,2013 +2018-08-10,2368.0,2356.0,2387.0,ascending,2013 +2018-08-22,2329.0,2312.0,2385.0,ascending,2013 +2018-09-03,2335.0,2315.0,2387.0,ascending,2013 +2018-09-15,2364.0,2340.0,2387.0,ascending,2013 +2018-10-09,2329.0,2312.0,2385.0,ascending,2013 +2018-10-21,2011.0,1859.0,2247.0,ascending,2013 +2018-11-02,1807.0,1272.0,2066.0,ascending,2013 +2018-11-14,1853.0,1500.0,2063.0,ascending,2013 +2018-12-08,1560.0,1186.0,1782.0,ascending,2013 +2018-12-20,1404.0,1182.0,1577.0,ascending,2013 +2019-01-01,1310.0,1182.0,1418.0,ascending,2013 +2019-01-13,1200.0,1182.0,1214.0,ascending,2013 +2019-01-25,1182.0,1182.0,1182.0,ascending,2013 +2019-02-06,1182.0,1182.0,1182.0,ascending,2013 +2019-02-18,1182.0,1182.0,1182.0,ascending,2013 +2019-03-02,1182.0,1182.0,1182.0,ascending,2013 +2019-03-14,1182.0,1182.0,1182.0,ascending,2013 +2019-03-26,1182.0,1182.0,1182.0,ascending,2013 +2019-04-07,1182.0,1182.0,1182.0,ascending,2013 +2019-04-19,1342.0,1251.0,1413.0,ascending,2013 +2019-05-01,2132.0,2048.0,2252.0,ascending,2013 +2019-05-13,1744.0,1678.0,1804.0,ascending,2013 +2019-05-25,2387.0,2387.0,2387.0,ascending,2013 +2019-06-06,2385.0,2371.0,2387.0,ascending,2013 +2019-06-18,2361.0,2333.0,2387.0,ascending,2013 +2019-06-30,2326.0,2307.0,2387.0,ascending,2013 +2019-07-12,2324.0,2307.0,2385.0,ascending,2013 +2019-07-24,2346.0,2318.0,2387.0,ascending,2013 +2019-08-05,2346.0,2318.0,2387.0,ascending,2013 +2019-08-17,2340.0,2317.0,2387.0,ascending,2013 +2019-08-29,2324.0,2299.0,2385.0,ascending,2013 +2019-09-10,2370.0,2364.0,2387.0,ascending,2013 +2019-09-22,2323.0,2307.0,2364.0,ascending,2013 +2019-10-04,2229.0,2190.0,2287.0,ascending,2013 +2019-10-16,1806.0,1305.0,2045.0,ascending,2013 +2019-10-28,1615.0,1224.0,1837.0,ascending,2013 +2019-11-09,1369.0,1200.0,1501.0,ascending,2013 +2019-11-21,1312.0,1182.0,1418.0,ascending,2013 +2019-12-03,1220.0,1182.0,1244.0,ascending,2013 +2019-12-15,1187.0,1182.0,1194.0,ascending,2013 +2019-12-27,1182.0,1182.0,1182.0,ascending,2013 +2020-01-08,1186.0,1182.0,1191.0,ascending,2013 +2020-01-20,1182.0,1182.0,1182.0,ascending,2013 +2020-02-01,1182.0,1182.0,1182.0,ascending,2013 +2020-02-13,1182.0,1182.0,1182.0,ascending,2013 +2020-02-25,1182.0,1182.0,1186.0,ascending,2013 +2020-03-08,1182.0,1182.0,1182.0,ascending,2013 +2020-03-20,1182.0,1182.0,1182.0,ascending,2013 +2020-04-01,1182.0,1182.0,1182.0,ascending,2013 +2020-04-13,1182.0,1182.0,1182.0,ascending,2013 +2020-04-25,1521.0,1509.0,1534.0,ascending,2013 +2020-05-07,1953.0,1880.0,2059.0,ascending,2013 +2020-05-19,2230.0,2180.0,2312.0,ascending,2013 +2020-05-31,2371.0,2369.0,2387.0,ascending,2013 +2020-06-12,2387.0,2387.0,2387.0,ascending,2013 +2020-06-24,2387.0,2387.0,2387.0,ascending,2013 +2020-07-06,2371.0,2369.0,2387.0,ascending,2013 +2020-07-18,2361.0,2335.0,2385.0,ascending,2013 +2020-07-30,2335.0,2315.0,2387.0,ascending,2013 +2020-08-11,2370.0,2364.0,2387.0,ascending,2013 +2020-08-23,2360.0,2329.0,2387.0,ascending,2013 +2020-09-04,2387.0,2387.0,2387.0,ascending,2013 +2020-09-16,2356.0,2324.0,2385.0,ascending,2013 +2020-09-28,2318.0,2293.0,2370.0,ascending,2013 +2020-10-10,2270.0,2228.0,2324.0,ascending,2013 +2020-11-03,2197.0,2147.0,2248.0,ascending,2013 +2020-11-15,1757.0,1194.0,2007.0,ascending,2013 +2020-11-27,1628.0,1186.0,1864.0,ascending,2013 +2020-12-09,1450.0,1186.0,1664.0,ascending,2013 +2020-12-21,1253.0,1182.0,1321.0,ascending,2013 +2021-01-02,1182.0,1182.0,1182.0,ascending,2013 +2021-01-14,1182.0,1182.0,1182.0,ascending,2013 +2021-01-26,1187.0,1182.0,1194.0,ascending,2013 +2021-02-07,1182.0,1182.0,1182.0,ascending,2013 +2021-02-19,1182.0,1182.0,1182.0,ascending,2013 +2021-03-03,1182.0,1182.0,1182.0,ascending,2013 +2021-03-15,1182.0,1182.0,1182.0,ascending,2013 +2021-03-27,1182.0,1182.0,1182.0,ascending,2013 +2021-04-08,1182.0,1182.0,1182.0,ascending,2013 +2021-04-20,1955.0,1851.0,2093.0,ascending,2013 +2021-05-02,1664.0,1517.0,1767.0,ascending,2013 +2021-05-14,2060.0,1980.0,2163.0,ascending,2013 +2021-05-26,2203.0,2151.0,2251.0,ascending,2013 +2021-06-07,2361.0,2333.0,2387.0,ascending,2013 +2021-06-19,2371.0,2369.0,2387.0,ascending,2013 +2021-07-01,2361.0,2333.0,2387.0,ascending,2013 +2021-07-13,2370.0,2364.0,2387.0,ascending,2013 +2021-07-25,2324.0,2303.0,2387.0,ascending,2013 +2021-08-06,2361.0,2333.0,2387.0,ascending,2013 +2021-08-18,2387.0,2387.0,2387.0,ascending,2013 +2021-08-30,2387.0,2387.0,2387.0,ascending,2013 +2021-09-11,2361.0,2333.0,2387.0,ascending,2013 +2021-09-23,2368.0,2356.0,2387.0,ascending,2013 +2021-10-05,2310.0,2281.0,2361.0,ascending,2013 +2021-10-17,2281.0,2237.0,2324.0,ascending,2013 +2021-10-29,2224.0,2185.0,2273.0,ascending,2013 +2021-11-10,1745.0,1182.0,1990.0,ascending,2013 +2021-11-22,1673.0,1187.0,1905.0,ascending,2013 +2021-12-04,1565.0,1182.0,1792.0,ascending,2013 +2021-12-16,1368.0,1182.0,1509.0,ascending,2013 +2017-06-14,2392.0,2391.0,2407.0,descending,2013 +2017-06-26,2388.0,2371.0,2407.0,descending,2013 +2017-07-08,2388.0,2371.0,2407.0,descending,2013 +2017-07-20,2371.0,2347.0,2407.0,descending,2013 +2017-08-01,2371.0,2339.0,2392.0,descending,2013 +2017-08-13,2377.0,2354.0,2407.0,descending,2013 +2017-08-25,2407.0,2407.0,2407.0,descending,2013 +2017-09-06,2383.0,2362.0,2407.0,descending,2013 +2017-09-18,2294.0,2250.0,2377.0,descending,2013 +2017-09-30,2236.0,2195.0,2299.0,descending,2013 +2017-10-12,2201.0,2153.0,2247.0,descending,2013 +2017-10-24,1541.0,1186.0,1766.0,descending,2013 +2017-11-05,1204.0,1185.0,1220.0,descending,2013 +2017-11-17,1191.0,1185.0,1199.0,descending,2013 +2017-11-29,1186.0,1185.0,1191.0,descending,2013 +2017-12-11,1185.0,1185.0,1185.0,descending,2013 +2017-12-23,1185.0,1185.0,1185.0,descending,2013 +2018-01-04,1185.0,1185.0,1185.0,descending,2013 +2018-01-16,1185.0,1185.0,1185.0,descending,2013 +2018-01-28,1185.0,1185.0,1185.0,descending,2013 +2018-02-09,1185.0,1185.0,1185.0,descending,2013 +2018-02-21,1185.0,1185.0,1185.0,descending,2013 +2018-03-05,1185.0,1185.0,1185.0,descending,2013 +2018-03-17,1185.0,1185.0,1185.0,descending,2013 +2018-03-29,1185.0,1185.0,1185.0,descending,2013 +2018-04-10,1185.0,1185.0,1185.0,descending,2013 +2018-04-22,1185.0,1185.0,1185.0,descending,2013 +2018-05-04,1185.0,1185.0,1185.0,descending,2013 +2018-05-16,1574.0,1533.0,1608.0,descending,2013 +2018-06-09,2407.0,2407.0,2407.0,descending,2013 +2018-06-21,2407.0,2407.0,2407.0,descending,2013 +2018-07-03,2407.0,2407.0,2407.0,descending,2013 +2018-07-15,2407.0,2407.0,2407.0,descending,2013 +2018-07-27,2371.0,2347.0,2407.0,descending,2013 +2018-08-08,2388.0,2371.0,2407.0,descending,2013 +2018-08-20,2391.0,2383.0,2407.0,descending,2013 +2018-09-01,2407.0,2407.0,2407.0,descending,2013 +2018-09-25,2285.0,2229.0,2407.0,descending,2013 +2018-10-07,2096.0,1963.0,2339.0,descending,2013 +2018-10-19,1998.0,1846.0,2244.0,descending,2013 +2018-10-31,1720.0,1289.0,1926.0,descending,2013 +2018-11-12,1932.0,1849.0,2039.0,descending,2013 +2018-12-06,1220.0,1185.0,1249.0,descending,2013 +2018-12-18,1185.0,1185.0,1186.0,descending,2013 +2018-12-30,1185.0,1185.0,1186.0,descending,2013 +2019-01-11,1185.0,1185.0,1185.0,descending,2013 +2019-01-23,1185.0,1185.0,1185.0,descending,2013 +2019-02-04,1185.0,1185.0,1185.0,descending,2013 +2019-02-16,1185.0,1185.0,1185.0,descending,2013 +2019-02-28,1185.0,1185.0,1185.0,descending,2013 +2019-03-12,1185.0,1185.0,1185.0,descending,2013 +2019-03-24,1185.0,1185.0,1185.0,descending,2013 +2019-04-05,1185.0,1185.0,1185.0,descending,2013 +2019-04-17,1185.0,1185.0,1185.0,descending,2013 +2019-04-29,1185.0,1185.0,1185.0,descending,2013 +2019-08-15,2388.0,2371.0,2407.0,descending,2013 +2019-08-27,2329.0,2309.0,2388.0,descending,2013 +2019-09-08,2391.0,2383.0,2407.0,descending,2013 +2019-09-20,2343.0,2320.0,2392.0,descending,2013 +2019-10-02,2209.0,2155.0,2262.0,descending,2013 +2019-10-14,1644.0,1243.0,1862.0,descending,2013 +2019-10-26,1403.0,1215.0,1567.0,descending,2013 +2019-11-07,1250.0,1199.0,1311.0,descending,2013 +2019-11-19,1207.0,1185.0,1230.0,descending,2013 +2019-12-01,1186.0,1185.0,1191.0,descending,2013 +2019-12-13,1185.0,1185.0,1185.0,descending,2013 +2019-12-25,1185.0,1185.0,1185.0,descending,2013 +2020-01-18,1185.0,1185.0,1186.0,descending,2013 +2020-01-30,1185.0,1185.0,1185.0,descending,2013 +2020-02-11,1185.0,1185.0,1186.0,descending,2013 +2020-02-23,1185.0,1185.0,1185.0,descending,2013 +2020-03-06,1185.0,1185.0,1185.0,descending,2013 +2020-03-18,1185.0,1185.0,1185.0,descending,2013 +2020-03-30,1185.0,1185.0,1185.0,descending,2013 +2020-04-11,1185.0,1185.0,1185.0,descending,2013 +2020-04-23,1530.0,1515.0,1539.0,descending,2013 +2020-05-05,1386.0,1342.0,1415.0,descending,2013 +2020-05-29,2323.0,2298.0,2392.0,descending,2013 +2020-06-10,2392.0,2391.0,2407.0,descending,2013 +2020-06-22,2407.0,2407.0,2407.0,descending,2013 +2020-07-04,2392.0,2391.0,2407.0,descending,2013 +2020-07-16,2407.0,2407.0,2407.0,descending,2013 +2020-08-09,2371.0,2347.0,2407.0,descending,2013 +2020-08-21,2383.0,2362.0,2407.0,descending,2013 +2020-09-02,2407.0,2407.0,2407.0,descending,2013 +2020-09-14,2392.0,2391.0,2407.0,descending,2013 +2020-09-26,2325.0,2303.0,2371.0,descending,2013 +2020-10-08,2304.0,2279.0,2359.0,descending,2013 +2020-10-20,2241.0,2197.0,2307.0,descending,2013 +2020-11-01,2126.0,2080.0,2200.0,descending,2013 +2020-11-13,1514.0,1186.0,1738.0,descending,2013 +2020-11-25,1249.0,1185.0,1311.0,descending,2013 +2020-12-07,1203.0,1185.0,1218.0,descending,2013 +2020-12-19,1185.0,1185.0,1186.0,descending,2013 +2020-12-31,1185.0,1185.0,1186.0,descending,2013 +2021-01-12,1185.0,1185.0,1185.0,descending,2013 +2021-01-24,1185.0,1185.0,1185.0,descending,2013 +2021-02-05,1185.0,1185.0,1185.0,descending,2013 +2021-02-17,1185.0,1185.0,1185.0,descending,2013 +2021-03-01,1185.0,1185.0,1185.0,descending,2013 +2021-03-13,1185.0,1185.0,1185.0,descending,2013 +2021-03-25,1185.0,1185.0,1185.0,descending,2013 +2021-04-06,1185.0,1185.0,1185.0,descending,2013 +2021-04-18,1185.0,1185.0,1185.0,descending,2013 +2021-04-30,1645.0,1526.0,1738.0,descending,2013 +2021-05-12,1814.0,1753.0,1861.0,descending,2013 +2021-05-24,1894.0,1854.0,1926.0,descending,2013 +2021-08-04,2391.0,2383.0,2407.0,descending,2013 +2021-08-28,2407.0,2407.0,2407.0,descending,2013 +2021-09-21,2392.0,2391.0,2407.0,descending,2013 +2021-10-03,2362.0,2343.0,2388.0,descending,2013 +2021-10-15,2292.0,2264.0,2321.0,descending,2013 +2021-10-27,2219.0,2178.0,2270.0,descending,2013 +2021-11-08,1665.0,1185.0,1902.0,descending,2013 +2021-11-20,1499.0,1185.0,1724.0,descending,2013 +2021-12-02,1350.0,1185.0,1479.0,descending,2013 +2021-12-14,1216.0,1186.0,1240.0,descending,2013 +2021-12-26,1191.0,1185.0,1199.0,descending,2013 +2022-01-07,1185.0,1185.0,1185.0,descending,2013 +2022-01-19,1186.0,1185.0,1191.0,descending,2013 +2022-01-31,1185.0,1185.0,1185.0,descending,2013 +2022-02-12,1185.0,1185.0,1185.0,descending,2013 +2022-02-24,1185.0,1185.0,1185.0,descending,2013 +2022-03-08,1185.0,1185.0,1185.0,descending,2013 +2022-03-20,1185.0,1185.0,1185.0,descending,2013 +2022-04-13,1185.0,1185.0,1185.0,descending,2013 +2022-05-07,1485.0,1439.0,1536.0,descending,2013 +2022-06-24,2391.0,2383.0,2407.0,descending,2013 +2022-08-11,2407.0,2407.0,2407.0,descending,2013 +2022-08-23,2377.0,2354.0,2407.0,descending,2013 +2022-09-04,2407.0,2407.0,2407.0,descending,2013 +2022-09-16,2407.0,2407.0,2407.0,descending,2013 +2022-09-28,2371.0,2347.0,2407.0,descending,2013 +2022-10-10,2304.0,2279.0,2371.0,descending,2013 +2022-10-22,2264.0,2221.0,2323.0,descending,2013 +2022-11-03,2236.0,2191.0,2307.0,descending,2013 +2022-11-15,2201.0,2136.0,2278.0,descending,2013 +2022-11-27,2202.0,2152.0,2255.0,descending,2013 +2022-12-09,2194.0,2145.0,2236.0,descending,2013 +2022-12-21,1760.0,1185.0,2011.0,descending,2013 +2023-01-02,1732.0,1186.0,1973.0,descending,2013 +2023-01-14,1637.0,1186.0,1876.0,descending,2013 +2023-01-26,1601.0,1186.0,1830.0,descending,2013 +2023-02-07,1296.0,1186.0,1406.0,descending,2013 +2023-02-19,1192.0,1185.0,1201.0,descending,2013 +2023-03-03,1185.0,1185.0,1185.0,descending,2013 +2023-03-15,1185.0,1185.0,1185.0,descending,2013 +2023-03-27,1185.0,1185.0,1185.0,descending,2013 +2023-04-08,1185.0,1185.0,1185.0,descending,2013 +2023-04-20,1185.0,1185.0,1185.0,descending,2013 +2023-05-02,1185.0,1185.0,1185.0,descending,2013 +2023-06-07,2307.0,2273.0,2392.0,descending,2013 +2023-06-19,2388.0,2371.0,2407.0,descending,2013 +2023-07-01,2407.0,2407.0,2407.0,descending,2013 +2023-07-13,2407.0,2407.0,2407.0,descending,2013 +2023-07-25,2359.0,2327.0,2407.0,descending,2013 +2023-08-06,2377.0,2354.0,2407.0,descending,2013 +2023-08-18,2383.0,2362.0,2407.0,descending,2013 +2023-08-30,2391.0,2383.0,2407.0,descending,2013 +2023-09-11,2392.0,2391.0,2407.0,descending,2013 +2023-09-23,2371.0,2339.0,2392.0,descending,2013 +2023-10-05,2299.0,2267.0,2354.0,descending,2013 +2023-10-17,2383.0,2383.0,2388.0,descending,2013 +2023-10-29,2407.0,2407.0,2407.0,descending,2013 +2023-11-10,2407.0,2407.0,2407.0,descending,2013 +2023-11-22,1510.0,1186.0,1734.0,descending,2013 +2023-12-16,1200.0,1185.0,1213.0,descending,2013 +2023-12-28,1185.0,1185.0,1185.0,descending,2013 +2024-01-21,1185.0,1185.0,1185.0,descending,2013 +2024-02-02,1185.0,1185.0,1185.0,descending,2013 +2024-02-14,1185.0,1185.0,1185.0,descending,2013 +2024-02-26,1185.0,1185.0,1185.0,descending,2013 +2024-03-09,1185.0,1185.0,1185.0,descending,2013 +2024-03-21,1185.0,1185.0,1185.0,descending,2013 +2024-04-02,1186.0,1185.0,1191.0,descending,2013 +2024-04-14,1185.0,1185.0,1185.0,descending,2013 +2024-04-26,1356.0,1289.0,1413.0,descending,2013 +2024-05-20,1979.0,1917.0,2058.0,descending,2013 +2024-06-01,2074.0,2031.0,2121.0,descending,2013 +2024-06-13,2407.0,2407.0,2407.0,descending,2013 +2024-06-25,2392.0,2391.0,2407.0,descending,2013 +2024-07-31,2377.0,2354.0,2407.0,descending,2013 +2024-08-12,2407.0,2407.0,2407.0,descending,2013 +2024-08-24,2383.0,2362.0,2407.0,descending,2013 +2024-09-05,2391.0,2383.0,2407.0,descending,2013 +2024-09-17,2354.0,2325.0,2407.0,descending,2013 +2024-09-29,2235.0,2176.0,2326.0,descending,2013 +2024-10-11,2270.0,2229.0,2309.0,descending,2013 +2024-11-04,2407.0,2407.0,2407.0,descending,2013 +2024-11-16,1685.0,1185.0,1918.0,descending,2013 +2024-11-28,1557.0,1185.0,1785.0,descending,2013 +2024-12-10,1425.0,1191.0,1629.0,descending,2013 +2024-12-22,1224.0,1186.0,1253.0,descending,2013 diff --git a/docs/_static/example_meltextent_and_snowline_1d/sample_snowline_elev.csv b/docs/_static/example_meltextent_and_snowline_1d/sample_snowline_elev.csv new file mode 100644 index 00000000..2a6fb36f --- /dev/null +++ b/docs/_static/example_meltextent_and_snowline_1d/sample_snowline_elev.csv @@ -0,0 +1,336 @@ +date,z,z_min,z_max,direction,ref_dem_year +2016-11-24,1182.0,1182.0,1182.0,ascending,2013 +2016-12-18,1182.0,1182.0,1182.0,ascending,2013 +2017-01-11,1182.0,1182.0,1182.0,ascending,2013 +2017-02-04,1182.0,1182.0,1182.0,ascending,2013 +2017-02-28,1182.0,1182.0,1182.0,ascending,2013 +2017-03-12,1182.0,1182.0,1182.0,ascending,2013 +2017-03-24,1182.0,1182.0,1182.0,ascending,2013 +2017-04-05,1182.0,1182.0,1182.0,ascending,2013 +2017-04-17,1199.0,1196.0,1204.0,ascending,2013 +2017-04-29,1203.0,1186.0,1239.0,ascending,2013 +2017-05-11,1429.0,1347.0,1546.0,ascending,2013 +2017-05-23,1270.0,1182.0,1354.0,ascending,2013 +2017-06-04,1429.0,1404.0,1491.0,ascending,2013 +2017-06-16,1600.0,1546.0,1645.0,ascending,2013 +2017-06-28,1637.0,1593.0,1698.0,ascending,2013 +2017-07-10,1747.0,1694.0,1811.0,ascending,2013 +2017-07-22,1804.0,1736.0,1868.0,ascending,2013 +2017-08-15,1704.0,1652.0,1746.0,ascending,2013 +2017-08-27,1740.0,1706.0,1780.0,ascending,2013 +2017-09-08,1714.0,1672.0,1774.0,ascending,2013 +2017-09-20,1611.0,1534.0,1718.0,ascending,2013 +2017-10-02,1729.0,1657.0,1877.0,ascending,2013 +2017-11-07,1182.0,1182.0,1182.0,ascending,2013 +2017-12-01,1182.0,1182.0,1182.0,ascending,2013 +2017-12-13,1182.0,1182.0,1182.0,ascending,2013 +2018-01-06,1182.0,1182.0,1182.0,ascending,2013 +2018-01-18,1182.0,1182.0,1182.0,ascending,2013 +2018-02-11,1182.0,1182.0,1182.0,ascending,2013 +2018-02-23,1182.0,1182.0,1182.0,ascending,2013 +2018-03-07,1182.0,1182.0,1182.0,ascending,2013 +2018-03-19,1182.0,1182.0,1182.0,ascending,2013 +2018-03-31,1182.0,1182.0,1182.0,ascending,2013 +2018-04-12,1278.0,1275.0,1281.0,ascending,2013 +2018-04-24,1239.0,1232.0,1336.0,ascending,2013 +2018-05-06,1182.0,1182.0,1182.0,ascending,2013 +2018-05-18,1182.0,1182.0,1276.0,ascending,2013 +2018-05-30,1182.0,1182.0,1435.0,ascending,2013 +2018-06-11,1420.0,1389.0,1469.0,ascending,2013 +2018-06-23,1479.0,1440.0,1527.0,ascending,2013 +2018-07-05,1573.0,1556.0,1610.0,ascending,2013 +2018-07-17,1603.0,1582.0,1628.0,ascending,2013 +2018-07-29,1705.0,1652.0,1772.0,ascending,2013 +2018-08-10,1754.0,1705.0,1813.0,ascending,2013 +2018-08-22,1741.0,1713.0,1855.0,ascending,2013 +2018-09-03,1657.0,1614.0,1706.0,ascending,2013 +2018-09-15,1806.0,1745.0,1883.0,ascending,2013 +2018-10-09,1766.0,1746.0,2049.0,ascending,2013 +2018-10-21,1332.0,1312.0,1786.0,ascending,2013 +2018-11-02,1270.0,1270.0,1270.0,ascending,2013 +2018-11-14,1488.0,1476.0,1497.0,ascending,2013 +2018-12-08,1182.0,1182.0,1182.0,ascending,2013 +2018-12-20,1182.0,1182.0,1182.0,ascending,2013 +2019-01-01,1182.0,1182.0,1182.0,ascending,2013 +2019-01-13,1182.0,1182.0,1182.0,ascending,2013 +2019-01-25,1182.0,1182.0,1182.0,ascending,2013 +2019-02-06,1182.0,1182.0,1182.0,ascending,2013 +2019-02-18,1182.0,1182.0,1182.0,ascending,2013 +2019-03-02,1182.0,1182.0,1182.0,ascending,2013 +2019-03-14,1182.0,1182.0,1182.0,ascending,2013 +2019-03-26,1182.0,1182.0,1182.0,ascending,2013 +2019-04-07,1182.0,1182.0,1182.0,ascending,2013 +2019-04-19,1250.0,1250.0,1250.0,ascending,2013 +2019-05-01,1187.0,1182.0,1839.0,ascending,2013 +2019-05-13,1182.0,1182.0,1286.0,ascending,2013 +2019-05-25,1295.0,1265.0,1342.0,ascending,2013 +2019-06-06,1444.0,1419.0,1521.0,ascending,2013 +2019-06-18,1541.0,1510.0,1578.0,ascending,2013 +2019-06-30,1681.0,1648.0,1812.0,ascending,2013 +2019-07-12,1818.0,1785.0,1923.0,ascending,2013 +2019-07-24,1935.0,1878.0,2047.0,ascending,2013 +2019-08-05,1908.0,1850.0,2012.0,ascending,2013 +2019-08-17,1894.0,1841.0,1980.0,ascending,2013 +2019-08-29,1905.0,1840.0,2040.0,ascending,2013 +2019-09-10,1905.0,1859.0,1997.0,ascending,2013 +2019-09-22,1518.0,1509.0,1738.0,ascending,2013 +2019-10-04,1698.0,1673.0,2044.0,ascending,2013 +2019-10-16,1304.0,1304.0,1304.0,ascending,2013 +2019-10-28,1223.0,1223.0,1223.0,ascending,2013 +2019-11-09,1199.0,1199.0,1199.0,ascending,2013 +2019-11-21,1182.0,1182.0,1182.0,ascending,2013 +2019-12-03,1182.0,1182.0,1182.0,ascending,2013 +2019-12-15,1182.0,1182.0,1182.0,ascending,2013 +2019-12-27,1182.0,1182.0,1182.0,ascending,2013 +2020-01-08,1182.0,1182.0,1182.0,ascending,2013 +2020-01-20,1182.0,1182.0,1182.0,ascending,2013 +2020-02-01,1182.0,1182.0,1182.0,ascending,2013 +2020-02-13,1182.0,1182.0,1182.0,ascending,2013 +2020-02-25,1182.0,1182.0,1182.0,ascending,2013 +2020-03-08,1182.0,1182.0,1182.0,ascending,2013 +2020-03-20,1182.0,1182.0,1182.0,ascending,2013 +2020-04-01,1182.0,1182.0,1182.0,ascending,2013 +2020-04-13,1182.0,1182.0,1182.0,ascending,2013 +2020-04-25,1182.0,1182.0,1295.0,ascending,2013 +2020-05-07,1182.0,1182.0,1718.0,ascending,2013 +2020-05-19,1265.0,1193.0,1628.0,ascending,2013 +2020-05-31,1396.0,1358.0,1430.0,ascending,2013 +2020-06-12,1457.0,1429.0,1535.0,ascending,2013 +2020-06-24,1334.0,1314.0,1356.0,ascending,2013 +2020-07-06,1602.0,1544.0,1702.0,ascending,2013 +2020-07-18,1608.0,1594.0,1773.0,ascending,2013 +2020-07-30,1721.0,1665.0,1868.0,ascending,2013 +2020-08-11,1706.0,1684.0,1840.0,ascending,2013 +2020-08-23,1884.0,1809.0,1990.0,ascending,2013 +2020-09-04,1583.0,1570.0,1664.0,ascending,2013 +2020-09-16,1731.0,1700.0,1906.0,ascending,2013 +2020-09-28,1724.0,1661.0,1945.0,ascending,2013 +2020-10-10,1730.0,1716.0,2014.0,ascending,2013 +2020-11-03,1846.0,1846.0,2136.0,ascending,2013 +2020-11-15,1193.0,1193.0,1193.0,ascending,2013 +2020-11-27,1182.0,1182.0,1182.0,ascending,2013 +2020-12-09,1182.0,1182.0,1182.0,ascending,2013 +2020-12-21,1182.0,1182.0,1182.0,ascending,2013 +2021-01-02,1182.0,1182.0,1182.0,ascending,2013 +2021-01-14,1182.0,1182.0,1182.0,ascending,2013 +2021-01-26,1182.0,1182.0,1182.0,ascending,2013 +2021-02-07,1182.0,1182.0,1182.0,ascending,2013 +2021-02-19,1182.0,1182.0,1182.0,ascending,2013 +2021-03-03,1182.0,1182.0,1182.0,ascending,2013 +2021-03-15,1182.0,1182.0,1182.0,ascending,2013 +2021-03-27,1182.0,1182.0,1182.0,ascending,2013 +2021-04-08,1182.0,1182.0,1182.0,ascending,2013 +2021-04-20,1182.0,1182.0,1698.0,ascending,2013 +2021-05-02,1186.0,1182.0,1314.0,ascending,2013 +2021-05-14,1219.0,1196.0,1588.0,ascending,2013 +2021-05-26,1344.0,1296.0,1603.0,ascending,2013 +2021-06-07,1414.0,1381.0,1485.0,ascending,2013 +2021-06-19,1524.0,1508.0,1554.0,ascending,2013 +2021-07-01,1625.0,1604.0,1671.0,ascending,2013 +2021-07-13,1653.0,1638.0,1687.0,ascending,2013 +2021-07-25,1720.0,1694.0,1798.0,ascending,2013 +2021-08-06,1819.0,1760.0,1885.0,ascending,2013 +2021-08-18,1559.0,1535.0,1576.0,ascending,2013 +2021-08-30,1729.0,1672.0,1807.0,ascending,2013 +2021-09-11,1761.0,1724.0,1795.0,ascending,2013 +2021-09-23,1868.0,1826.0,1924.0,ascending,2013 +2021-10-05,1840.0,1828.0,1987.0,ascending,2013 +2021-10-17,1987.0,1941.0,2137.0,ascending,2013 +2021-10-29,1881.0,1878.0,2114.0,ascending,2013 +2021-11-10,1182.0,1182.0,1182.0,ascending,2013 +2021-11-22,1186.0,1186.0,1186.0,ascending,2013 +2021-12-04,1182.0,1182.0,1182.0,ascending,2013 +2021-12-16,1182.0,1182.0,1182.0,ascending,2013 +2017-06-14,1715.0,1671.0,1929.0,descending,2013 +2017-06-26,1878.0,1805.0,1951.0,descending,2013 +2017-07-08,1885.0,1859.0,2059.0,descending,2013 +2017-07-20,1894.0,1872.0,2049.0,descending,2013 +2017-08-01,1895.0,1878.0,2090.0,descending,2013 +2017-08-13,1862.0,1861.0,2136.0,descending,2013 +2017-08-25,1869.0,1836.0,1906.0,descending,2013 +2017-09-06,1931.0,1919.0,2151.0,descending,2013 +2017-09-18,1889.0,1857.0,2153.0,descending,2013 +2017-09-30,1720.0,1698.0,2115.0,descending,2013 +2017-10-12,1898.0,1894.0,2151.0,descending,2013 +2017-10-24,1185.0,1185.0,1185.0,descending,2013 +2017-11-05,1185.0,1185.0,1185.0,descending,2013 +2017-11-17,1185.0,1185.0,1185.0,descending,2013 +2017-11-29,1185.0,1185.0,1185.0,descending,2013 +2017-12-11,1185.0,1185.0,1185.0,descending,2013 +2017-12-23,1185.0,1185.0,1185.0,descending,2013 +2018-01-04,1185.0,1185.0,1185.0,descending,2013 +2018-01-16,1185.0,1185.0,1185.0,descending,2013 +2018-01-28,1185.0,1185.0,1185.0,descending,2013 +2018-02-09,1185.0,1185.0,1185.0,descending,2013 +2018-02-21,1185.0,1185.0,1185.0,descending,2013 +2018-03-05,1185.0,1185.0,1185.0,descending,2013 +2018-03-17,1185.0,1185.0,1185.0,descending,2013 +2018-03-29,1185.0,1185.0,1185.0,descending,2013 +2018-04-10,1185.0,1185.0,1185.0,descending,2013 +2018-04-22,1185.0,1185.0,1185.0,descending,2013 +2018-05-04,1185.0,1185.0,1185.0,descending,2013 +2018-05-16,1185.0,1185.0,1230.0,descending,2013 +2018-06-09,1299.0,1282.0,1345.0,descending,2013 +2018-06-21,1391.0,1356.0,1416.0,descending,2013 +2018-07-03,1567.0,1542.0,1596.0,descending,2013 +2018-07-15,1603.0,1575.0,1629.0,descending,2013 +2018-07-27,1788.0,1705.0,1872.0,descending,2013 +2018-08-08,1782.0,1738.0,1855.0,descending,2013 +2018-08-20,1859.0,1809.0,1926.0,descending,2013 +2018-09-01,1727.0,1671.0,1805.0,descending,2013 +2018-09-25,1653.0,1264.0,2003.0,descending,2013 +2018-10-07,1813.0,1682.0,1929.0,descending,2013 +2018-10-19,1248.0,1244.0,1708.0,descending,2013 +2018-10-31,1288.0,1288.0,1288.0,descending,2013 +2018-11-12,1685.0,1425.0,1845.0,descending,2013 +2018-12-06,1185.0,1185.0,1185.0,descending,2013 +2018-12-18,1185.0,1185.0,1185.0,descending,2013 +2018-12-30,1185.0,1185.0,1185.0,descending,2013 +2019-01-11,1185.0,1185.0,1185.0,descending,2013 +2019-01-23,1185.0,1185.0,1185.0,descending,2013 +2019-02-04,1185.0,1185.0,1185.0,descending,2013 +2019-02-16,1185.0,1185.0,1185.0,descending,2013 +2019-02-28,1185.0,1185.0,1185.0,descending,2013 +2019-03-12,1185.0,1185.0,1185.0,descending,2013 +2019-03-24,1185.0,1185.0,1185.0,descending,2013 +2019-04-05,1185.0,1185.0,1185.0,descending,2013 +2019-04-17,1185.0,1185.0,1185.0,descending,2013 +2019-04-29,1185.0,1185.0,1185.0,descending,2013 +2019-08-15,1908.0,1864.0,1996.0,descending,2013 +2019-08-27,1887.0,1857.0,2115.0,descending,2013 +2019-09-08,1916.0,1877.0,2043.0,descending,2013 +2019-09-20,1487.0,1466.0,1780.0,descending,2013 +2019-10-02,1715.0,1687.0,2077.0,descending,2013 +2019-10-14,1240.0,1240.0,1240.0,descending,2013 +2019-10-26,1213.0,1213.0,1213.0,descending,2013 +2019-11-07,1193.0,1193.0,1193.0,descending,2013 +2019-11-19,1185.0,1185.0,1185.0,descending,2013 +2019-12-01,1185.0,1185.0,1185.0,descending,2013 +2019-12-13,1185.0,1185.0,1185.0,descending,2013 +2019-12-25,1185.0,1185.0,1185.0,descending,2013 +2020-01-18,1185.0,1185.0,1185.0,descending,2013 +2020-01-30,1185.0,1185.0,1185.0,descending,2013 +2020-02-11,1185.0,1185.0,1185.0,descending,2013 +2020-02-23,1185.0,1185.0,1185.0,descending,2013 +2020-03-06,1185.0,1185.0,1185.0,descending,2013 +2020-03-18,1185.0,1185.0,1185.0,descending,2013 +2020-03-30,1185.0,1185.0,1185.0,descending,2013 +2020-04-11,1185.0,1185.0,1185.0,descending,2013 +2020-04-23,1185.0,1185.0,1238.0,descending,2013 +2020-05-05,1191.0,1185.0,1261.0,descending,2013 +2020-05-29,1255.0,1231.0,1425.0,descending,2013 +2020-06-10,1378.0,1345.0,1439.0,descending,2013 +2020-06-22,1269.0,1249.0,1290.0,descending,2013 +2020-07-04,1568.0,1517.0,1607.0,descending,2013 +2020-07-16,1641.0,1603.0,1804.0,descending,2013 +2020-08-09,1788.0,1725.0,1866.0,descending,2013 +2020-08-21,1851.0,1791.0,1926.0,descending,2013 +2020-09-02,1575.0,1567.0,1709.0,descending,2013 +2020-09-14,1729.0,1717.0,1949.0,descending,2013 +2020-09-26,1639.0,1552.0,1998.0,descending,2013 +2020-10-08,1773.0,1738.0,2104.0,descending,2013 +2020-10-20,1904.0,1893.0,2176.0,descending,2013 +2020-11-01,1905.0,1905.0,2077.0,descending,2013 +2020-11-13,1185.0,1185.0,1185.0,descending,2013 +2020-11-25,1185.0,1185.0,1185.0,descending,2013 +2020-12-07,1185.0,1185.0,1185.0,descending,2013 +2020-12-19,1185.0,1185.0,1185.0,descending,2013 +2020-12-31,1185.0,1185.0,1185.0,descending,2013 +2021-01-12,1185.0,1185.0,1185.0,descending,2013 +2021-01-24,1185.0,1185.0,1185.0,descending,2013 +2021-02-05,1185.0,1185.0,1185.0,descending,2013 +2021-02-17,1185.0,1185.0,1185.0,descending,2013 +2021-03-01,1185.0,1185.0,1185.0,descending,2013 +2021-03-13,1185.0,1185.0,1185.0,descending,2013 +2021-03-25,1185.0,1185.0,1185.0,descending,2013 +2021-04-06,1185.0,1185.0,1185.0,descending,2013 +2021-04-18,1185.0,1185.0,1185.0,descending,2013 +2021-04-30,1185.0,1185.0,1304.0,descending,2013 +2021-05-12,1189.0,1185.0,1403.0,descending,2013 +2021-05-24,1339.0,1264.0,1558.0,descending,2013 +2021-08-04,1821.0,1774.0,1903.0,descending,2013 +2021-08-28,1722.0,1695.0,1753.0,descending,2013 +2021-09-21,1848.0,1812.0,1930.0,descending,2013 +2021-10-03,1897.0,1872.0,2077.0,descending,2013 +2021-10-15,1987.0,1957.0,2176.0,descending,2013 +2021-10-27,1986.0,1975.0,2141.0,descending,2013 +2021-11-08,1185.0,1185.0,1185.0,descending,2013 +2021-11-20,1185.0,1185.0,1185.0,descending,2013 +2021-12-02,1185.0,1185.0,1185.0,descending,2013 +2021-12-14,1185.0,1185.0,1185.0,descending,2013 +2021-12-26,1185.0,1185.0,1185.0,descending,2013 +2022-01-07,1185.0,1185.0,1185.0,descending,2013 +2022-01-19,1185.0,1185.0,1185.0,descending,2013 +2022-01-31,1185.0,1185.0,1185.0,descending,2013 +2022-02-12,1185.0,1185.0,1185.0,descending,2013 +2022-02-24,1185.0,1185.0,1185.0,descending,2013 +2022-03-08,1185.0,1185.0,1185.0,descending,2013 +2022-03-20,1185.0,1185.0,1185.0,descending,2013 +2022-04-13,1185.0,1185.0,1185.0,descending,2013 +2022-05-07,1185.0,1185.0,1255.0,descending,2013 +2022-06-24,1671.0,1613.0,1739.0,descending,2013 +2022-08-11,1552.0,1542.0,1565.0,descending,2013 +2022-08-23,1834.0,1800.0,1888.0,descending,2013 +2022-09-04,1644.0,1629.0,1660.0,descending,2013 +2022-09-16,1320.0,1306.0,1556.0,descending,2013 +2022-09-28,1426.0,1398.0,1682.0,descending,2013 +2022-10-10,1207.0,1203.0,1794.0,descending,2013 +2022-10-22,1883.0,1840.0,2138.0,descending,2013 +2022-11-03,1775.0,1756.0,2109.0,descending,2013 +2022-11-15,1892.0,1880.0,2108.0,descending,2013 +2022-11-27,1900.0,1894.0,2131.0,descending,2013 +2022-12-09,1897.0,1896.0,2121.0,descending,2013 +2022-12-21,1185.0,1185.0,1185.0,descending,2013 +2023-01-02,1185.0,1185.0,1185.0,descending,2013 +2023-01-14,1185.0,1185.0,1185.0,descending,2013 +2023-01-26,1185.0,1185.0,1185.0,descending,2013 +2023-02-07,1185.0,1185.0,1185.0,descending,2013 +2023-02-19,1185.0,1185.0,1185.0,descending,2013 +2023-03-03,1185.0,1185.0,1185.0,descending,2013 +2023-03-15,1185.0,1185.0,1185.0,descending,2013 +2023-03-27,1185.0,1185.0,1185.0,descending,2013 +2023-04-08,1185.0,1185.0,1185.0,descending,2013 +2023-04-20,1185.0,1185.0,1185.0,descending,2013 +2023-05-02,1185.0,1185.0,1185.0,descending,2013 +2023-06-07,1207.0,1193.0,1473.0,descending,2013 +2023-06-19,1330.0,1299.0,1364.0,descending,2013 +2023-07-01,1481.0,1459.0,1510.0,descending,2013 +2023-07-13,1659.0,1608.0,1738.0,descending,2013 +2023-07-25,1796.0,1717.0,1891.0,descending,2013 +2023-08-06,1852.0,1795.0,1932.0,descending,2013 +2023-08-18,1813.0,1777.0,1898.0,descending,2013 +2023-08-30,1907.0,1852.0,2022.0,descending,2013 +2023-09-11,1613.0,1561.0,1796.0,descending,2013 +2023-09-23,1677.0,1645.0,1945.0,descending,2013 +2023-10-05,1859.0,1840.0,2096.0,descending,2013 +2023-10-17,1898.0,1883.0,2240.0,descending,2013 +2023-10-29,1963.0,1941.0,2377.0,descending,2013 +2023-11-10,1918.0,1913.0,2354.0,descending,2013 +2023-11-22,1185.0,1185.0,1185.0,descending,2013 +2023-12-16,1185.0,1185.0,1185.0,descending,2013 +2023-12-28,1185.0,1185.0,1185.0,descending,2013 +2024-01-21,1185.0,1185.0,1185.0,descending,2013 +2024-02-02,1185.0,1185.0,1185.0,descending,2013 +2024-02-14,1185.0,1185.0,1185.0,descending,2013 +2024-02-26,1185.0,1185.0,1185.0,descending,2013 +2024-03-09,1185.0,1185.0,1185.0,descending,2013 +2024-03-21,1185.0,1185.0,1185.0,descending,2013 +2024-04-02,1185.0,1185.0,1185.0,descending,2013 +2024-04-14,1185.0,1185.0,1185.0,descending,2013 +2024-04-26,1267.0,1249.0,1286.0,descending,2013 +2024-05-20,1429.0,1189.0,1734.0,descending,2013 +2024-06-01,1248.0,1224.0,1517.0,descending,2013 +2024-06-13,1387.0,1347.0,1414.0,descending,2013 +2024-06-25,1615.0,1567.0,1691.0,descending,2013 +2024-07-31,1805.0,1745.0,1861.0,descending,2013 +2024-08-12,1489.0,1466.0,1567.0,descending,2013 +2024-08-24,1650.0,1589.0,1769.0,descending,2013 +2024-09-05,1623.0,1606.0,1809.0,descending,2013 +2024-09-17,1332.0,1248.0,1593.0,descending,2013 +2024-09-29,1685.0,1618.0,1857.0,descending,2013 +2024-10-11,1729.0,1716.0,1957.0,descending,2013 +2024-11-04,1897.0,1888.0,2282.0,descending,2013 +2024-11-16,1185.0,1185.0,1185.0,descending,2013 +2024-11-28,1185.0,1185.0,1185.0,descending,2013 +2024-12-10,1189.0,1189.0,1189.0,descending,2013 +2024-12-22,1185.0,1185.0,1185.0,descending,2013 diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index be0f01ed..036a5c16 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -565,17 +565,17 @@ def run(list_packed_vars): gcm = class_climate.GCM(name=ref_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table + gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table, verbose=debug ) if pygem_prms['mb']['option_ablation'] == 2 and ref_climate_name in ['ERA5']: gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table + gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table, verbose=debug ) else: gcm_tempstd = np.zeros(gcm_temp.shape) # Precipitation [m] gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table + gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table, verbose=debug ) # Elevation [m asl] gcm_elev = gcm.importGCMfxnearestneighbor_xarray( @@ -583,7 +583,12 @@ def run(list_packed_vars): ) # Lapse rate [degC m-1] (always monthly) gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, upscale_var_timestep=True + gcm.lr_fn, + gcm.lr_vn, + main_glac_rgi, + dates_table, + upscale_var_timestep=True, + verbose=debug, ) # ===== LOOP THROUGH GLACIERS TO RUN CALIBRATION ===== diff --git a/pygem/bin/run/run_calibration_frontalablation.py b/pygem/bin/run/run_calibration_frontalablation.py index efe2084b..9e347e7b 100644 --- a/pygem/bin/run/run_calibration_frontalablation.py +++ b/pygem/bin/run/run_calibration_frontalablation.py @@ -136,17 +136,17 @@ def reg_calving_flux( gcm = class_climate.GCM(name=args.ref_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table + gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table, verbose=debug ) if pygem_prms['mb']['option_ablation'] == 2 and args.ref_climate_name in ['ERA5']: gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table + gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table, verbose=debug ) else: gcm_tempstd = np.zeros(gcm_temp.shape) # Precipitation [m] gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table + gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table, verbose=debug ) # Elevation [m asl] gcm_elev = gcm.importGCMfxnearestneighbor_xarray( @@ -154,7 +154,7 @@ def reg_calving_flux( ) # Lapse rate [degC m-1] gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table + gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, verbose=debug ) # ===== CALIBRATE ALL THE GLACIERS AT ONCE ===== diff --git a/pygem/bin/run/run_calibration_reg_glena.py b/pygem/bin/run/run_calibration_reg_glena.py index 5efd9dae..29cfd19e 100644 --- a/pygem/bin/run/run_calibration_reg_glena.py +++ b/pygem/bin/run/run_calibration_reg_glena.py @@ -341,17 +341,21 @@ def main(): gcm = class_climate.GCM(name=sim_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi_subset, dates_table + gcm.temp_fn, gcm.temp_vn, main_glac_rgi_subset, dates_table, verbose=debug ) if pygem_prms['mbmod']['option_ablation'] == 2 and sim_climate_name in ['ERA5']: gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi_subset, dates_table + gcm.tempstd_fn, + gcm.tempstd_vn, + main_glac_rgi_subset, + dates_table, + verbose=debug, ) else: gcm_tempstd = np.zeros(gcm_temp.shape) # Precipitation [m] gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi_subset, dates_table + gcm.prec_fn, gcm.prec_vn, main_glac_rgi_subset, dates_table, verbose=debug ) # Elevation [m asl] gcm_elev = gcm.importGCMfxnearestneighbor_xarray( @@ -359,7 +363,7 @@ def main(): ) # Lapse rate [degC m-1] gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi_subset, dates_table + gcm.lr_fn, gcm.lr_vn, main_glac_rgi_subset, dates_table, verbose=debug ) # ===== RUN MASS BALANCE ===== diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index 323224fc..aff4c5e6 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -70,11 +70,11 @@ def run( # Air temperature [degC] temp, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.temp_fn, ref_clim.temp_vn, main_glac_rgi, dt + ref_clim.temp_fn, ref_clim.temp_vn, main_glac_rgi, dt, verbose=debug ) # Precipitation [m] prec, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt + ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt, verbose=debug ) # Elevation [m asl] elev = ref_clim.importGCMfxnearestneighbor_xarray( @@ -82,7 +82,7 @@ def run( ) # Lapse rate [degC m-1] lr, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt + ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt, verbose=debug ) # load prior regionally averaged modelprms (from Rounce et al. 2023) diff --git a/pygem/bin/run/run_simulation.py b/pygem/bin/run/run_simulation.py index 0f365d7c..bc5f9f38 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -430,17 +430,17 @@ def run(list_packed_vars): # ----- Select Temperature and Precipitation Data ----- # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table_full + gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table_full, verbose=debug ) ref_temp, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( - ref_gcm.temp_fn, ref_gcm.temp_vn, main_glac_rgi, dates_table_ref + ref_gcm.temp_fn, ref_gcm.temp_vn, main_glac_rgi, dates_table_ref, verbose=debug ) # Precipitation [m] gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table_full + gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table_full, verbose=debug ) ref_prec, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( - ref_gcm.prec_fn, ref_gcm.prec_vn, main_glac_rgi, dates_table_ref + ref_gcm.prec_fn, ref_gcm.prec_vn, main_glac_rgi, dates_table_ref, verbose=debug ) # Elevation [m asl] try: @@ -548,13 +548,17 @@ def run(list_packed_vars): ref_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table_ref.shape[0])) elif pygem_prms['mb']['option_ablation'] == 2 and sim_climate_name in ['ERA5']: gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table + gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table, verbose=debug ) ref_tempstd = gcm_tempstd elif pygem_prms['mb']['option_ablation'] == 2 and args.ref_climate_name in ['ERA5']: # Compute temp std based on reference climate data ref_tempstd, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( - ref_gcm.tempstd_fn, ref_gcm.tempstd_vn, main_glac_rgi, dates_table_ref + ref_gcm.tempstd_fn, + ref_gcm.tempstd_vn, + main_glac_rgi, + dates_table_ref, + verbose=debug, ) # Monthly average from reference climate data gcm_tempstd = gcmbiasadj.monthly_avg_array_rolled( @@ -567,13 +571,18 @@ def run(list_packed_vars): # Lapse rate if sim_climate_name in ['ERA-Interim', 'ERA5']: gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table + gcm.lr_fn, + gcm.lr_vn, + main_glac_rgi, + dates_table, + upscale_var_timestep=True, + verbose=debug, ) ref_lr = gcm_lr else: # Compute lapse rates based on reference climate data ref_lr, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( - ref_gcm.lr_fn, ref_gcm.lr_vn, main_glac_rgi, dates_table_ref + ref_gcm.lr_fn, ref_gcm.lr_vn, main_glac_rgi, dates_table_ref, verbose=debug ) # Monthly average from reference climate data gcm_lr = gcmbiasadj.monthly_avg_array_rolled( @@ -591,12 +600,9 @@ def run(list_packed_vars): else: nsims = 1 - # Number of years (for OGGM's run_until_and_store) - if pygem_prms['time']['timestep'] == 'monthly': - nyears = int(dates_table.shape[0] / 12) - nyears_ref = int(dates_table_ref.shape[0] / 12) - else: - assert True == False, 'Adjust nyears for non-monthly timestep' + # Number of years + nyears = dates_table.year.unique()[-1] - dates_table.year.unique()[0] + 1 + nyears_ref = dates_table_ref.year.unique()[-1] - dates_table.year.unique()[0] + 1 for glac in range(main_glac_rgi.shape[0]): if glac == 0: diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index 102e93a8..76f48554 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -39,11 +39,11 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): # Air temperature [degC] temp, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.temp_fn, ref_clim.temp_vn, main_glac_rgi, dt + ref_clim.temp_fn, ref_clim.temp_vn, main_glac_rgi, dt, verbose=debug ) # Precipitation [m] prec, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt + ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt, verbose=debug ) # Elevation [m asl] elev = ref_clim.importGCMfxnearestneighbor_xarray( @@ -51,7 +51,7 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): ) # Lapse rate [degC m-1] lr, _ = ref_clim.importGCMvarnearestneighbor_xarray( - ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt + ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt, verbose=debug ) # load prior regionally averaged modelprms (from Rounce et al. 2023) diff --git a/pygem/class_climate.py b/pygem/class_climate.py index d2564eff..7d964067 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -9,6 +9,7 @@ class of climate data and functions associated with manipulating the dataset to """ import os +import warnings import numpy as np @@ -192,10 +193,7 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.elev_fn = pygem_prms['climate']['paths']['era5_elev_fn'] self.lr_fn = pygem_prms['climate']['paths']['era5_lr_fn'] # Variable filepaths - if pygem_prms['climate']['paths']['era5_fullpath']: - self.var_fp = '' - self.fx_fp = '' - else: + if pygem_prms['climate']['paths']['era5_relpath']: self.var_fp = ( pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] @@ -204,6 +202,10 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] ) + else: + self.var_fp = '' + self.fx_fp = '' + # Extra information self.timestep = pygem_prms['time']['timestep'] self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] @@ -232,6 +234,7 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): pygem_prms['root'] + pygem_prms['climate']['paths']['eraint_relpath'] ) + # Extra information self.timestep = pygem_prms['time']['timestep'] self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] @@ -454,6 +457,7 @@ def importGCMvarnearestneighbor_xarray( dates_table, realizations=['r1i1p1f1', 'r4i1p1f1'], upscale_var_timestep=False, + verbose=False, ): """ Import time series of variables and extract nearest neighbor. @@ -691,6 +695,37 @@ def importGCMvarnearestneighbor_xarray( start_idx : end_idx + 1, latlon[0], latlon[1] ].values + # Check all glacier use appropriate climate data + for i, latlon in enumerate(latlon_nearidx): + rgi_id = main_glac_rgi[ + pygem_prms['rgi']['rgi_glacno_float_colname'] + ].values[i] + if (len(data[vn][self.lat_vn].values) == 1) or ( + len(data[vn][self.lon_vn].values) == 1 + ): + if verbose: + warnings.warn( + f'{vn} data has only one latitude or longitude value; check that the correct data is being used', + Warning, + stacklevel=2, + ) + else: + lat_res = abs(np.diff(data[vn][self.lat_vn].values)[0]) + lon_res = abs(np.diff(data[vn][self.lon_vn].values)[0]) + lat_dd = abs( + main_glac_rgi[self.rgi_lat_colname].values[i] + - data[vn][self.lat_vn].values[latlon[0]] + ) + lon_dd = abs( + main_glac_rgi[self.rgi_lon_colname].values[i] + - data[vn][self.lon_vn].values[latlon[1]] + ) + + assert lat_dd <= lat_res and lon_dd <= lon_res, ( + f'Climate data pixel for {vn} too from glacier {rgi_id}: Δlat={lat_dd:.3f}, ' + + f'Δlon={lon_dd:.3f}, res=({lat_res:.3f}, {lon_res:.3f})' + ) + # Convert to series glac_variable_series = np.array( [glac_variable_dict[x] for x in latlon_nearidx] diff --git a/pygem/oggm_compat.py b/pygem/oggm_compat.py index 7c2c7ddf..e7c95a3c 100755 --- a/pygem/oggm_compat.py +++ b/pygem/oggm_compat.py @@ -25,7 +25,7 @@ from pygem.setup.config import ConfigManager # from oggm.shop import rgitopo -from pygem.shop import debris, icethickness, mbdata +from pygem.shop import debris, icethickness, mbdata, meltextent_and_snowline_1d # instantiate ConfigManager config_manager = ConfigManager() @@ -127,6 +127,16 @@ def single_flowline_glacier_directory( ): workflow.execute_entity_task(debris.debris_to_gdir, gdir) workflow.execute_entity_task(debris.debris_binned, gdir) + # 1d melt extent calibration data + if not os.path.isfile(gdir.get_filepath('meltextent_1d')): + workflow.execute_entity_task( + meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir + ) + # 1d snowline calibration data + if not os.path.isfile(gdir.get_filepath('snowline_1d')): + workflow.execute_entity_task( + meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir + ) return gdir @@ -138,7 +148,7 @@ def single_flowline_glacier_directory_with_calving( k_calving=1, logging_level=pygem_prms['oggm']['logging_level'], has_internet=pygem_prms['oggm']['has_internet'], - working_dir=pygem_prms['root'] + pygem_prms['oggm']['oggm_gdir_relpath'], + working_dir=f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}', facorrected=pygem_prms['setup']['include_frontalablation'], ): """Prepare a GlacierDirectory for PyGEM (single flowline to start with) @@ -223,6 +233,16 @@ def single_flowline_glacier_directory_with_calving( workflow.execute_entity_task( mbdata.mb_df_to_gdir, gdir, **{'facorrected': facorrected} ) + # 1d melt extent calibration data + if not os.path.isfile(gdir.get_filepath('meltextent_1d')): + workflow.execute_entity_task( + meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir + ) + # 1d snowline calibration data + if not os.path.isfile(gdir.get_filepath('snowline_1d')): + workflow.execute_entity_task( + meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir + ) return gdir diff --git a/pygem/setup/config.py b/pygem/setup/config.py index 608daf7b..5ab8e51e 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -157,8 +157,7 @@ def _validate_config(self, config): 'climate.sim_wateryear': str, 'climate.constantarea_years': int, 'climate.paths': dict, - 'climate.paths.era5_fullpath': bool, - 'climate.paths.era5_relpath': str, + 'climate.paths.era5_relpath': (str, type(None)), 'climate.paths.era5_temp_fn': str, 'climate.paths.era5_tempstd_fn': str, 'climate.paths.era5_prec_fn': str, @@ -223,6 +222,8 @@ def _validate_config(self, config): 'calib.emulator_params.ftol_opt': float, 'calib.emulator_params.eps_opt': float, 'calib.MCMC_params': dict, + 'calib.MCMC_params.option_calib_meltextent_1d': bool, + 'calib.MCMC_params.option_calib_snowline_1d': bool, 'calib.MCMC_params.option_use_emulator': bool, 'calib.MCMC_params.emulator_sims': int, 'calib.MCMC_params.tbias_step': float, @@ -263,6 +264,8 @@ def _validate_config(self, config): 'calib.data.icethickness': dict, 'calib.data.icethickness.h_ref_relpath': str, 'calib.icethickness_cal_frac_byarea': float, + 'calib.data.meltextent_1d.meltextent_1d_relpath': (str, type(None)), + 'calib.data.snowline_1d.snowline_1d_relpath': (str, type(None)), 'sim': dict, 'sim.option_dynamics': (str, type(None)), 'sim.option_bias_adjustment': int, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 9755acee..5fce2d1c 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -66,7 +66,6 @@ climate: # ===== CLIMATE DATA FILEPATHS AND FILENAMES ===== paths: # ERA5 (default reference climate data) - era5_fullpath: False # bool. 'True' ignores 'root' and 'era5_relpath' for ERA5 data and assumes below filenames are absolute era5_relpath: /climate_data/ERA5/ era5_temp_fn: ERA5_temp_monthly.nc era5_tempstd_fn: ERA5_tempstd_monthly.nc @@ -151,6 +150,8 @@ calib: # MCMC params MCMC_params: + option_calib_meltextent_1d: false # option to calibrate against 1d melt extent data (true or false) + option_calib_snowline_1d: false # option to calibrate against 1d snowline data (true or false) option_use_emulator: true # use emulator or full model (if true, calibration must have first been run with option_calibretion=='emulator') emulator_sims: 100 tbias_step: 0.1 @@ -199,6 +200,13 @@ calib: # ice thickness icethickness: h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ + # 1d melt extents + meltextent_1d: + meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) + # 1d snowlines + snowline_1d: + snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) + icethickness_cal_frac_byarea: 0.9 # Regional glacier area fraction that is used to calibrate the ice thickness # e.g., 0.9 means only the largest 90% of glaciers by area will be used to calibrate diff --git a/pygem/shop/meltextent_and_snowline_1d.py b/pygem/shop/meltextent_and_snowline_1d.py new file mode 100644 index 00000000..863eb6e3 --- /dev/null +++ b/pygem/shop/meltextent_and_snowline_1d.py @@ -0,0 +1,289 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2025 Brandon Tober , David Rounce + +Distributed under the MIT license +""" + +# Built-in libaries +import datetime +import logging +import os + +import pandas as pd + +# External libraries +# Local libraries +from oggm import cfg +from oggm.utils import entity_task + +# pygem imports +from pygem.setup.config import ConfigManager + +# instantiate ConfigManager +config_manager = ConfigManager() +# read the config +pygem_prms = config_manager.read_config() + + +# Module logger +log = logging.getLogger(__name__) + +# Add the new name "snowline_1d" to the list of things that the GlacierDirectory understands +if 'meltextent_1d' not in cfg.BASENAMES: + cfg.BASENAMES['meltextent_1d'] = ( + 'meltextent_1d.json', + '1D snowline data', + ) +if 'snowline_1d' not in cfg.BASENAMES: + cfg.BASENAMES['snowline_1d'] = ( + 'snowline_1d.json', + '1D snowline data', + ) + + +@entity_task(log, writes=['snowline_1d']) +def meltextent_1d_to_gdir( + gdir, +): + """ + Add 1d melt extent observations to the given glacier directory + + Parameters + ---------- + gdir : :py:class:`oggm.GlacierDirectory` + where to write the data + + expected csv structure: + Columns: 'date', 'z', 'z_min', 'z_max', 'direction' + 'date': Observation date, stored as a string in 'YYYY-MM-DD' format + 'z': Melt extent elevation (meters) + 'z_min': Melt extent elevation minimum (meters) + 'z_max': Melt extent elevation maximum (meters) + 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') + 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) + """ + # get dataset file path + meltextent_1d_fp = ( + f'{pygem_prms["root"]}/' + f'{pygem_prms["calib"]["data"]["meltextent_1d"]["meltextent_1d_relpath"]}/' + f'{gdir.rgi_id.split("-")[1]}_melt_extent_elev.csv' + ) + + # check for file + if os.path.exists(meltextent_1d_fp): + meltextent_1d_df = pd.read_csv(meltextent_1d_fp) + else: + log.debug('No melt extent data to load, skipping task.') + raise Warning('No melt extent data to load') # file not found, skip + + validate_meltextent_1d_structure(meltextent_1d_df) + meltextent_1d_dict = meltextent_csv_to_dict(meltextent_1d_df) + gdir.write_json(meltextent_1d_dict, 'meltextent_1d') + + +def validate_meltextent_1d_structure(data): + """Validate that meltextent_1d CSV structure matches expected format.""" + + required_cols = ['date', 'z', 'z_min', 'z_max', 'direction', 'ref_dem_year'] + for col in required_cols: + if col not in data.columns: + raise ValueError(f"Missing required column '{col}' in melt extent CSV.") + + # Validate dates + dates = data['date'] + if not isinstance(dates, pd.Series) or len(dates) == 0: + raise ValueError("'dates' must be a non-empty series.") + for i, date_str in enumerate(dates): + try: + datetime.datetime.strptime(date_str, '%Y-%m-%d') + except ValueError: + raise ValueError( + f"Invalid date format in 'dates[{i}]': {date_str}" + ) from None + + # Validate z + z = data['z'] + if not (isinstance(z, pd.Series) and len(z) == len(dates)): + raise ValueError(f"'z' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z): + raise ValueError("All 'z' values must be numeric.") + + # Validate z_min + z_min = data['z_min'] + if not (isinstance(z_min, pd.Series) and len(z_min) == len(dates)): + raise ValueError(f"'z_min' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z_min): + raise ValueError("All 'z_min' values must be numeric.") + + # Validate z_max + z_max = data['z_max'] + if not (isinstance(z_max, pd.Series) and len(z_max) == len(dates)): + raise ValueError(f"'z_max' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z_max): + raise ValueError("All 'z_max' values must be numeric.") + + # Validate direction + direction = data['direction'] + if not (isinstance(direction, pd.Series) and len(direction) == len(dates)): + raise ValueError(f"'direction' must be a series of length {len(dates)}.") + if not all(isinstance(x, str) for x in direction): + raise ValueError("All 'direction' values must be strings.") + + # Validate reference DEM year + dem_year = data['ref_dem_year'].dropna().unique() + if len(dem_year) != 1: + raise ValueError( + f"'ref_dem_year' must have exactly one unique value, " + f'but found {len(dem_year)}: {dem_year}' + ) + if not isinstance(dem_year, (int)): + raise TypeError( + f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__})." + ) + + return True + + +def meltextent_csv_to_dict(data): + """Convert snowline_1d CSV to JSON for OGGM ingestion.""" + dates = data['date'].astype(str).tolist() + z = data['z'].astype(float).tolist() + z_min = data['z_min'].astype(float).tolist() + z_max = data['z_max'].astype(float).tolist() + direction = data['direction'].astype(str).tolist() + ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] + + data_dict = { + 'date': dates, + 'z': z, + 'z_min': z_min, + 'z_max': z_max, + 'direction': direction, + 'ref_dem_year': ref_dem_year, + } + return data_dict + + +@entity_task(log, writes=['snowline_1d']) +def snowline_1d_to_gdir( + gdir, +): + """ + Add 1d snowline observations to the given glacier directory + + Parameters + ---------- + gdir : :py:class:`oggm.GlacierDirectory` + where to write the data + + expected csv structure: + Columns: 'date', 'z', 'z_min', 'z_max', 'direction' + 'date': Observation date, stored as a string in 'YYYY-MM-DD' format + 'z': Snowline elevation (m a.s.l.) + 'z_min': Snowline elevation minimum (m a.s.l.) + 'z_max': Snowline elevation maximum (m a.s.l.) + 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') + 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) + """ + # get dataset file path + snowline_1d_fp = ( + f'{pygem_prms["root"]}/' + f'{pygem_prms["calib"]["data"]["snowline_1d"]["snowline_1d_relpath"]}/' + f'{gdir.rgi_id.split("-")[1]}_snowline_elev.csv' + ) + + # check for file + if os.path.exists(snowline_1d_fp): + snowline_1d_df = pd.read_csv(snowline_1d_fp) + else: + log.debug('No snowline data to load, skipping task.') + raise Warning('No snowline data to load') # file not found, skip + + validate_snowline_1d_structure(snowline_1d_df) + snowline_1d_dict = snowline_csv_to_dict(snowline_1d_df) + gdir.write_json(snowline_1d_dict, 'snowline_1d') + + +def validate_snowline_1d_structure(data): + """Validate that snowline_1d CSV structure matches expected format.""" + + required_cols = ['date', 'z', 'z_min', 'z_max', 'direction', 'ref_dem_year'] + for col in required_cols: + if col not in data.columns: + raise ValueError(f"Missing required column '{col}' in snowline CSV.") + + # Validate dates + dates = data['date'] + if not isinstance(dates, pd.Series) or len(dates) == 0: + raise ValueError("'dates' must be a non-empty series.") + for i, date_str in enumerate(dates): + try: + datetime.datetime.strptime(date_str, '%Y-%m-%d') + except ValueError: + raise ValueError( + f"Invalid date format in 'dates[{i}]': {date_str}" + ) from None + + # Validate z + z = data['z'] + if not (isinstance(z, pd.Series) and len(z) == len(dates)): + raise ValueError(f"'z' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z): + raise ValueError("All 'z' values must be numeric.") + + # Validate z_min + z_min = data['z_min'] + if not (isinstance(z_min, pd.Series) and len(z_min) == len(dates)): + raise ValueError(f"'z_min' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z_min): + raise ValueError("All 'z_min' values must be numeric.") + + # Validate z_max + z_max = data['z_max'] + if not (isinstance(z_max, pd.Series) and len(z_max) == len(dates)): + raise ValueError(f"'z_max' must be a series of length {len(dates)}.") + if not all(isinstance(x, (int, float)) for x in z_max): + raise ValueError("All 'z_max' values must be numeric.") + + # Validate direction + direction = data['direction'] + if not (isinstance(direction, pd.Series) and len(direction) == len(dates)): + raise ValueError(f"'direction' must be a series of length {len(dates)}.") + if not all(isinstance(x, str) for x in direction): + raise ValueError("All 'direction' values must be strings.") + + # Validate reference DEM year + dem_year = data['ref_dem_year'].dropna().unique() + if len(dem_year) != 1: + raise ValueError( + f"'ref_dem_year' must have exactly one unique value, " + f'but found {len(dem_year)}: {dem_year}' + ) + if not isinstance(dem_year, (int)): + raise TypeError( + f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__})." + ) + + return True + + +def snowline_csv_to_dict(data): + """Convert snowline_1d CSV to JSON for OGGM ingestion.""" + dates = data['date'].astype(str).tolist() + z = data['z'].astype(float).tolist() + z_min = data['z_min'].astype(float).tolist() + z_max = data['z_max'].astype(float).tolist() + direction = data['direction'].astype(str).tolist() + ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] + + data_dict = { + 'date': dates, + 'z': z, + 'z_min': z_min, + 'z_max': z_max, + 'direction': direction, + 'ref_dem_year': ref_dem_year, + } + return data_dict diff --git a/pyproject.toml b/pyproject.toml index dc276675..85565cb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ ignore = [ "B007", # Loop control variable not used within loop body "B008", # Function call `range` in argument defaults "B023", # Function definition does not bind loop variable + "B905", # Missing explicit `strict=` parameter in `zip()` call "C405", # Unnecessary list literal "C408", # Unnecessary `dict()` call "C414", # Unnecessary `list()` call @@ -97,4 +98,4 @@ ignore = [ [tool.coverage.report] omit = ["pygem/tests/*"] show_missing = true -skip_empty = true +skip_empty = true \ No newline at end of file From e210814bc60e92a1330b66baa106a6ac00a0aed6 Mon Sep 17 00:00:00 2001 From: Albin Wells <91861031+albinwwells@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:32:45 -0400 Subject: [PATCH 05/19] Added reference DEM field to melt extent and snowline data imports (#140) --- pygem/shop/meltextent_and_snowline_1d.py | 40 ++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pygem/shop/meltextent_and_snowline_1d.py b/pygem/shop/meltextent_and_snowline_1d.py index 863eb6e3..3fec0cb0 100644 --- a/pygem/shop/meltextent_and_snowline_1d.py +++ b/pygem/shop/meltextent_and_snowline_1d.py @@ -62,6 +62,7 @@ def meltextent_1d_to_gdir( 'z_min': Melt extent elevation minimum (meters) 'z_max': Melt extent elevation maximum (meters) 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') + 'ref_dem': Reference DEM used for elevation values 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) """ # get dataset file path @@ -86,7 +87,15 @@ def meltextent_1d_to_gdir( def validate_meltextent_1d_structure(data): """Validate that meltextent_1d CSV structure matches expected format.""" - required_cols = ['date', 'z', 'z_min', 'z_max', 'direction', 'ref_dem_year'] + required_cols = [ + 'date', + 'z', + 'z_min', + 'z_max', + 'direction', + 'ref_dem', + 'ref_dem_year', + ] for col in required_cols: if col not in data.columns: raise ValueError(f"Missing required column '{col}' in melt extent CSV.") @@ -131,6 +140,13 @@ def validate_meltextent_1d_structure(data): if not all(isinstance(x, str) for x in direction): raise ValueError("All 'direction' values must be strings.") + # Validate reference DEM + ref_dem = data['ref_dem'].dropna().unique() + if not isinstance(ref_dem, (str)): + raise TypeError( + f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__})." + ) + # Validate reference DEM year dem_year = data['ref_dem_year'].dropna().unique() if len(dem_year) != 1: @@ -153,6 +169,7 @@ def meltextent_csv_to_dict(data): z_min = data['z_min'].astype(float).tolist() z_max = data['z_max'].astype(float).tolist() direction = data['direction'].astype(str).tolist() + ref_dem = data['ref_dem'].astype(str).tolist()[0] ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] data_dict = { @@ -161,6 +178,7 @@ def meltextent_csv_to_dict(data): 'z_min': z_min, 'z_max': z_max, 'direction': direction, + 'ref_dem': ref_dem, 'ref_dem_year': ref_dem_year, } return data_dict @@ -185,6 +203,7 @@ def snowline_1d_to_gdir( 'z_min': Snowline elevation minimum (m a.s.l.) 'z_max': Snowline elevation maximum (m a.s.l.) 'direction': SAR path direction, stored as a string (e.g., 'ascending' or 'descending') + 'ref_dem': Reference DEM used for elevation values 'ref_dem_year': Reference DEM year for elevation value of observations (m a.s.l.) (e.g., 2013 if using COP30) """ # get dataset file path @@ -209,7 +228,15 @@ def snowline_1d_to_gdir( def validate_snowline_1d_structure(data): """Validate that snowline_1d CSV structure matches expected format.""" - required_cols = ['date', 'z', 'z_min', 'z_max', 'direction', 'ref_dem_year'] + required_cols = [ + 'date', + 'z', + 'z_min', + 'z_max', + 'direction', + 'ref_dem', + 'ref_dem_year', + ] for col in required_cols: if col not in data.columns: raise ValueError(f"Missing required column '{col}' in snowline CSV.") @@ -254,6 +281,13 @@ def validate_snowline_1d_structure(data): if not all(isinstance(x, str) for x in direction): raise ValueError("All 'direction' values must be strings.") + # Validate reference DEM + ref_dem = data['ref_dem'].dropna().unique() + if not isinstance(ref_dem, (str)): + raise TypeError( + f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__})." + ) + # Validate reference DEM year dem_year = data['ref_dem_year'].dropna().unique() if len(dem_year) != 1: @@ -276,6 +310,7 @@ def snowline_csv_to_dict(data): z_min = data['z_min'].astype(float).tolist() z_max = data['z_max'].astype(float).tolist() direction = data['direction'].astype(str).tolist() + ref_dem = data['ref_dem'].astype(str).tolist()[0] ref_dem_year = data['ref_dem_year'].astype(int).tolist()[0] data_dict = { @@ -284,6 +319,7 @@ def snowline_csv_to_dict(data): 'z_min': z_min, 'z_max': z_max, 'direction': direction, + 'ref_dem': ref_dem, 'ref_dem_year': ref_dem_year, } return data_dict From fd6a26c0e02f10c1db1151f9737e3f2c123bce0e Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Thu, 16 Oct 2025 10:21:24 -0400 Subject: [PATCH 06/19] Framework for 1D elevation change calibration (#137) Closes #130. Closes #67. New shop module to load 1d elevation change data. Ability to calibrate against this new data type incorporated into MCMC framework. New plotting module created. --- .../01.00570_elev_change_1d.csv | 11 + .../01.00570_elev_change_1d.json | 1 + docs/_static/elev_change_1d/README.txt | 9 + pygem/bin/op/compress_gdirs.py | 12 +- pygem/bin/op/initialize.py | 12 +- pygem/bin/op/list_failed_simulations.py | 8 +- .../postproc/postproc_binned_monthly_mass.py | 30 +- .../postproc/postproc_compile_simulations.py | 33 +- pygem/bin/postproc/postproc_distribute_ice.py | 28 +- pygem/bin/postproc/postproc_monthly_mass.py | 22 +- pygem/bin/preproc/preproc_fetch_mbdata.py | 4 +- pygem/bin/preproc/preproc_wgms_estimate_kp.py | 145 +-- pygem/bin/run/run_calibration.py | 990 ++++++++------- .../run/run_calibration_frontalablation.py | 803 ++++-------- pygem/bin/run/run_calibration_reg_glena.py | 58 +- pygem/bin/run/run_inversion.py | 94 +- pygem/bin/run/run_mcmc_priors.py | 68 +- pygem/bin/run/run_simulation.py | 1104 ++++++----------- pygem/bin/run/run_spinup.py | 401 +++++- pygem/class_climate.py | 259 +--- pygem/gcmbiasadj.py | 196 +-- pygem/glacierdynamics.py | 274 ++-- pygem/massbalance.py | 556 +++------ pygem/mcmc.py | 402 ++---- pygem/oggm_compat.py | 62 +- pygem/output.py | 219 ++-- pygem/plot/graphics.py | 471 +++++++ pygem/pygem_modelsetup.py | 64 +- pygem/setup/config.py | 27 +- pygem/setup/config.yaml | 26 +- pygem/shop/debris.py | 16 +- pygem/shop/elevchange1d.py | 307 +++++ pygem/shop/icethickness.py | 25 +- pygem/shop/loso25icebridge.py | 622 ++++++++++ pygem/shop/mbdata.py | 31 +- pygem/shop/meltextent_and_snowline_1d.py | 34 +- pygem/shop/oib.py | 353 ------ pygem/tests/test_02_config.py | 23 +- pygem/tests/test_03_notebooks.py | 6 +- pygem/tests/test_04_postproc.py | 12 +- pygem/utils/_funcs.py | 62 +- pyproject.toml | 2 +- 42 files changed, 3645 insertions(+), 4237 deletions(-) create mode 100644 docs/_static/elev_change_1d/01.00570_elev_change_1d.csv create mode 100644 docs/_static/elev_change_1d/01.00570_elev_change_1d.json create mode 100644 docs/_static/elev_change_1d/README.txt create mode 100644 pygem/plot/graphics.py create mode 100644 pygem/shop/elevchange1d.py create mode 100644 pygem/shop/loso25icebridge.py delete mode 100644 pygem/shop/oib.py diff --git a/docs/_static/elev_change_1d/01.00570_elev_change_1d.csv b/docs/_static/elev_change_1d/01.00570_elev_change_1d.csv new file mode 100644 index 00000000..a17b819f --- /dev/null +++ b/docs/_static/elev_change_1d/01.00570_elev_change_1d.csv @@ -0,0 +1,11 @@ +bin_start,bin_stop,bin_area,date_start,date_end,dh,dh_sigma,ref_dem,ref_dem_year +1300.0,1400.0,79778.125,2016-06-01,2017-06-01,-4.50164794921875,2.37652587890625,COP30,2013 +1400.0,1500.0,107009.375,2016-06-01,2017-06-01,-3.091949462890625,1.5350341796875,COP30,2013 +1500.0,1600.0,96590.625,2016-06-01,2017-06-01,-2.728790283203125,1.53717041015625,COP30,2013 +1600.0,1700.0,138362.5,2016-06-01,2017-06-01,-1.95526123046875,3.421600341796875,COP30,2013 +1700.0,1800.0,176418.75,2016-06-01,2017-06-01,-2.159393310546875,7.1052093505859375,COP30,2013 +1800.0,1900.0,231671.875,2016-06-01,2017-06-01,-1.4642333984375,1.008819580078125,COP30,2013 +1900.0,2000.0,270537.5,2016-06-01,2017-06-01,-1.630615234375,1.78204345703125,COP30,2013 +2000.0,2100.0,218462.5,2016-06-01,2017-06-01,-1.223602294921875,1.511993408203125,COP30,2013 +2100.0,2200.0,137959.375,2016-06-01,2017-06-01,-1.411376953125,1.4329833984375,COP30,2013 +2200.0,2300.0,100803.125,2016-06-01,2017-06-01,-1.162353515625,2.39971923828125,COP30,2013 diff --git a/docs/_static/elev_change_1d/01.00570_elev_change_1d.json b/docs/_static/elev_change_1d/01.00570_elev_change_1d.json new file mode 100644 index 00000000..4446681f --- /dev/null +++ b/docs/_static/elev_change_1d/01.00570_elev_change_1d.json @@ -0,0 +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]]} \ No newline at end of file diff --git a/docs/_static/elev_change_1d/README.txt b/docs/_static/elev_change_1d/README.txt new file mode 100644 index 00000000..faf32cad --- /dev/null +++ b/docs/_static/elev_change_1d/README.txt @@ -0,0 +1,9 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2025 Brandon Tober , David Rounce + +Distributed under the MIT license +""" + +This directory contains example 1d elevation change data following the format specifications of PyGEM. \ No newline at end of file diff --git a/pygem/bin/op/compress_gdirs.py b/pygem/bin/op/compress_gdirs.py index 862af78c..ffb675c7 100644 --- a/pygem/bin/op/compress_gdirs.py +++ b/pygem/bin/op/compress_gdirs.py @@ -24,9 +24,7 @@ # Initialize OGGM subprocess cfg.initialize(logging_level='WARNING') -cfg.PATHS['working_dir'] = ( - f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' -) +cfg.PATHS['working_dir'] = f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' cfg.PARAMS['border'] = pygem_prms['oggm']['border'] cfg.PARAMS['use_multiprocessing'] = True @@ -34,9 +32,7 @@ def compress_region(region): print(f'\n=== Compressing glacier directories for RGI Region: {region} ===') # Get glacier IDs from the RGI shapefile - rgi_ids = gpd.read_file( - utils.get_rgi_region_file(str(region).zfill(2), version='62') - )['RGIId'].tolist() + rgi_ids = gpd.read_file(utils.get_rgi_region_file(str(region).zfill(2), version='62'))['RGIId'].tolist() # Initialize glacier directories gdirs = workflow.init_glacier_directories(rgi_ids) @@ -52,9 +48,7 @@ def compress_region(region): def main(): - parser = argparse.ArgumentParser( - description='Script to compress and store OGGM glacier directories' - ) + parser = argparse.ArgumentParser(description='Script to compress and store OGGM glacier directories') # add arguments parser.add_argument( '-rgi_region01', diff --git a/pygem/bin/op/initialize.py b/pygem/bin/op/initialize.py index 614a7447..0d0e38ca 100644 --- a/pygem/bin/op/initialize.py +++ b/pygem/bin/op/initialize.py @@ -88,9 +88,7 @@ def download_and_unzip_from_google_drive(file_id, output_dir): response = session.get(base_url, params={'id': file_id}, stream=True) token = get_confirm_token(response) if token: - response = session.get( - base_url, params={'id': file_id, 'confirm': token}, stream=True - ) + response = session.get(base_url, params={'id': file_id, 'confirm': token}, stream=True) save_response_content(response, zip_path) # Unzip the file @@ -99,11 +97,7 @@ def download_and_unzip_from_google_drive(file_id, output_dir): zip_ref.extractall(tmppath) # get root dir name of zipped files - dir = [ - item - for item in os.listdir(tmppath) - if os.path.isdir(os.path.join(tmppath, item)) - ][0] + dir = [item for item in os.listdir(tmppath) if os.path.isdir(os.path.join(tmppath, item))][0] unzip_dir = os.path.join(tmppath, dir) # get unique name if root dir name already exists in output_dir output_dir = get_unique_folder_name(os.path.join(output_dir, dir)) @@ -121,7 +115,7 @@ def main(): # Define the base directory basedir = os.path.join(os.path.expanduser('~'), 'PyGEM') # Google Drive file id for sample dataset - file_id = '1Wu4ZqpOKxnc4EYhcRHQbwGq95FoOxMfZ' + file_id = '16l2nEdWACwkpdNd8pdIX0ajyfGIsUf_B' # download and unzip out = download_and_unzip_from_google_drive(file_id, basedir) diff --git a/pygem/bin/op/list_failed_simulations.py b/pygem/bin/op/list_failed_simulations.py index 757cb103..1fc0196e 100644 --- a/pygem/bin/op/list_failed_simulations.py +++ b/pygem/bin/op/list_failed_simulations.py @@ -63,18 +63,14 @@ def run( # instantiate list of galcnos that are not in sim_dir failed_glacnos = [] - fps = glob.glob( - sim_dir + f'*_{calib_opt}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc' - ) + fps = glob.glob(sim_dir + f'*_{calib_opt}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc') # Glaciers with successful runs to process glacno_ran = [x.split('/')[-1].split('_')[0] for x in fps] glacno_ran = [x.split('.')[0].zfill(2) + '.' + x[-5:] for x in glacno_ran] # print stats of successfully simualated glaciers - main_glac_rgi = main_glac_rgi_all.loc[ - main_glac_rgi_all.apply(lambda x: x.rgino_str in glacno_ran, axis=1) - ] + main_glac_rgi = main_glac_rgi_all.loc[main_glac_rgi_all.apply(lambda x: x.rgino_str in glacno_ran, axis=1)] print( 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)}%)' ) diff --git a/pygem/bin/postproc/postproc_binned_monthly_mass.py b/pygem/bin/postproc/postproc_binned_monthly_mass.py index b123bced..b24b9bd2 100644 --- a/pygem/bin/postproc/postproc_binned_monthly_mass.py +++ b/pygem/bin/postproc/postproc_binned_monthly_mass.py @@ -34,9 +34,7 @@ def getparser(): """ Use argparse to add arguments from the command line """ - parser = argparse.ArgumentParser( - description='process monthly ice thickness for PyGEM simulation' - ) + parser = argparse.ArgumentParser(description='process monthly ice thickness for PyGEM simulation') # add arguments parser.add_argument( '-simpath', @@ -106,16 +104,13 @@ def get_binned_monthly(dotb_monthly, m_annual, h_annual): """ ### get monthly ice thickness ### # convert mass balance from m w.e. yr^-1 to m ice yr^-1 - dotb_monthly = dotb_monthly * ( - pygem_prms['constants']['density_water'] - / pygem_prms['constants']['density_ice'] - ) + dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) assert dotb_monthly.shape[2] % 12 == 0, 'Number of months is not a multiple of 12!' # obtain annual mass balance rate, sum monthly for each year - dotb_annual = dotb_monthly.reshape( - dotb_monthly.shape[0], dotb_monthly.shape[1], -1, 12 - ).sum(axis=-1) # climatic mass balance [m ice a^-1] + dotb_annual = dotb_monthly.reshape(dotb_monthly.shape[0], dotb_monthly.shape[1], -1, 12).sum( + axis=-1 + ) # climatic mass balance [m ice a^-1] # compute the thickness change per year delta_h_annual = np.diff(h_annual, axis=-1) # [m ice a^-1] (nbins, nyears-1) @@ -223,12 +218,7 @@ def update_xrdataset(input_ds, h_monthly, m_spec_monthly, m_monthly): count_vn = 0 encoding = {} for vn in output_coords_dict.keys(): - empty_holder = np.zeros( - [ - len(output_coords_dict[vn][i]) - for i in list(output_coords_dict[vn].keys()) - ] - ) + empty_holder = np.zeros([len(output_coords_dict[vn][i]) for i in list(output_coords_dict[vn].keys())]) output_ds = xr.Dataset( {vn: (list(output_coords_dict[vn].keys()), empty_holder)}, coords=output_coords_dict[vn], @@ -280,17 +270,13 @@ def run(simpath): ) # update dataset to add monthly mass change - output_ds_binned, encoding_binned = update_xrdataset( - binned_ds, h_monthly, m_spec_monthly, m_monthly - ) + output_ds_binned, encoding_binned = update_xrdataset(binned_ds, h_monthly, m_spec_monthly, m_monthly) # close input ds before write binned_ds.close() # append to existing binned netcdf - output_ds_binned.to_netcdf( - simpath, mode='a', encoding=encoding_binned, engine='netcdf4' - ) + output_ds_binned.to_netcdf(simpath, mode='a', encoding=encoding_binned, engine='netcdf4') # close datasets output_ds_binned.close() diff --git a/pygem/bin/postproc/postproc_compile_simulations.py b/pygem/bin/postproc/postproc_compile_simulations.py index a9665f87..391fff75 100644 --- a/pygem/bin/postproc/postproc_compile_simulations.py +++ b/pygem/bin/postproc/postproc_compile_simulations.py @@ -189,9 +189,7 @@ def run(args): nbatches = last_thous // batch_interval # split glaciers into groups of a thousand based on all glaciers in region - glacno_list_batches = modelsetup.split_list( - glacno_list_all, n=nbatches, group_thousands=True - ) + glacno_list_batches = modelsetup.split_list(glacno_list_all, n=nbatches, group_thousands=True) # make sure batch sublists are sorted properly and that each goes from NN001 to N(N+1)000 glacno_list_batches = sorted(glacno_list_batches, key=lambda x: x[0]) @@ -227,10 +225,7 @@ def run(args): )[0] else: fp = glob.glob( - base_dir - + gcm - + '/stats/' - + f'*{gcm}_{calibration}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc' + base_dir + gcm + '/stats/' + f'*{gcm}_{calibration}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc' )[0] # get number of sets from file name nsets = fp.split('/')[-1].split('_')[-4] @@ -290,13 +285,9 @@ def run(args): if nbatch == 0: # Glaciers with successful runs to process glacno_ran = [x.split('/')[-1].split('_')[0] for x in fps] - glacno_ran = [ - x.split('.')[0].zfill(2) + '.' + x[-5:] for x in glacno_ran - ] + glacno_ran = [x.split('.')[0].zfill(2) + '.' + x[-5:] for x in glacno_ran] main_glac_rgi = main_glac_rgi_all.loc[ - main_glac_rgi_all.apply( - lambda x: x.rgino_str in glacno_ran, axis=1 - ) + main_glac_rgi_all.apply(lambda x: x.rgino_str in glacno_ran, axis=1) ] print( f'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)}%)' @@ -432,20 +423,14 @@ def run(args): # time attributes - different for monthly v annual ds.time.attrs['long_name'] = 'time' if 'annual' in var: - ds.time.attrs['range'] = ( - str(year_values[0]) + ' - ' + str(year_values[-1]) - ) + ds.time.attrs['range'] = str(year_values[0]) + ' - ' + str(year_values[-1]) ds.time.attrs['comment'] = 'years referring to the start of each year' elif 'monthly' in var: - ds.time.attrs['range'] = ( - str(time_values[0]) + ' - ' + str(time_values[-1]) - ) + ds.time.attrs['range'] = str(time_values[0]) + ' - ' + str(time_values[-1]) ds.time.attrs['comment'] = 'start of the month' ds.RGIId.attrs['long_name'] = 'Randolph Glacier Inventory Id' - ds.RGIId.attrs['comment'] = ( - 'RGIv6.0 (https://nsidc.org/data/nsidc-0770/versions/6)' - ) + ds.RGIId.attrs['comment'] = 'RGIv6.0 (https://nsidc.org/data/nsidc-0770/versions/6)' ds.RGIId.attrs['cf_role'] = 'timeseries_id' if realizations[0]: @@ -504,9 +489,7 @@ def run(args): fp_merge_list_start = [int(f.split('-')[-2]) for f in fp_merge_list] if len(fp_merge_list) > 0: - fp_merge_list = [ - x for _, x in sorted(zip(fp_merge_list_start, fp_merge_list)) - ] + fp_merge_list = [x for _, x in sorted(zip(fp_merge_list_start, fp_merge_list))] ds = None for fp in fp_merge_list: diff --git a/pygem/bin/postproc/postproc_distribute_ice.py b/pygem/bin/postproc/postproc_distribute_ice.py index ec909397..688ef89b 100644 --- a/pygem/bin/postproc/postproc_distribute_ice.py +++ b/pygem/bin/postproc/postproc_distribute_ice.py @@ -42,9 +42,7 @@ def getparser(): """ Use argparse to add arguments from the command line """ - parser = argparse.ArgumentParser( - description='distrube PyGEM simulated ice thickness to a 2D grid' - ) + parser = argparse.ArgumentParser(description='distrube PyGEM simulated ice thickness to a 2D grid') # add arguments parser.add_argument( '-simpath', @@ -111,18 +109,9 @@ def pygem_to_oggm(pygem_simpath, oggm_diag=None, debug=False): def plot_distributed_thickness(ds): f, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4)) - vmax = ( - round( - np.nanmax(ds.simulated_thickness.sel(time=ds.coords['time'].values[0])) / 25 - ) - * 25 - ) - ds.simulated_thickness.sel(time=ds.coords['time'].values[0]).plot( - ax=ax1, vmin=0, vmax=vmax, add_colorbar=False - ) - ds.simulated_thickness.sel(time=ds.coords['time'].values[-1]).plot( - ax=ax2, vmin=0, vmax=vmax - ) + vmax = round(np.nanmax(ds.simulated_thickness.sel(time=ds.coords['time'].values[0])) / 25) * 25 + ds.simulated_thickness.sel(time=ds.coords['time'].values[0]).plot(ax=ax1, vmin=0, vmax=vmax, add_colorbar=False) + ds.simulated_thickness.sel(time=ds.coords['time'].values[-1]).plot(ax=ax2, vmin=0, vmax=vmax) ax1.axis('equal') ax2.axis('equal') plt.tight_layout() @@ -135,16 +124,11 @@ def run(simpath, debug=False): pygem_fn_split = pygem_fn.split('_') f_suffix = '_'.join(pygem_fn_split[1:])[:-3] glac_no = pygem_fn_split[0] - glacier_rgi_table = modelsetup.selectglaciersrgitable(glac_no=[glac_no]).loc[ - 0, : - ] + glacier_rgi_table = modelsetup.selectglaciersrgitable(glac_no=[glac_no]).loc[0, :] glacier_str = '{0:0.5f}'.format(glacier_rgi_table['RGIId_float']) # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== try: - if ( - glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_tidewater'] - ): + if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_tidewater']: gdir = single_flowline_glacier_directory(glacier_str) gdir.is_tidewater = False else: diff --git a/pygem/bin/postproc/postproc_monthly_mass.py b/pygem/bin/postproc/postproc_monthly_mass.py index d3a3678f..819c2591 100644 --- a/pygem/bin/postproc/postproc_monthly_mass.py +++ b/pygem/bin/postproc/postproc_monthly_mass.py @@ -91,9 +91,7 @@ def get_monthly_mass(glac_mass_annual, glac_massbaltotal_monthly): # get running total monthly mass balance - reshape into subarrays of all values for a given year, then take cumulative sum oshape = glac_massbaltotal_monthly.shape running_glac_massbaltotal_monthly = ( - np.reshape(glac_massbaltotal_monthly, (-1, 12), order='C') - .cumsum(axis=-1) - .reshape(oshape) + np.reshape(glac_massbaltotal_monthly, (-1, 12), order='C').cumsum(axis=-1).reshape(oshape) ) # tile annual mass to then superimpose atop running glacier mass balance (trim off final year from annual mass) @@ -128,9 +126,7 @@ def update_xrdataset(input_ds, glac_mass_monthly): time_values = input_ds.time.values output_coords_dict = collections.OrderedDict() - output_coords_dict['glac_mass_monthly'] = collections.OrderedDict( - [('glac', glac_values), ('time', time_values)] - ) + output_coords_dict['glac_mass_monthly'] = collections.OrderedDict([('glac', glac_values), ('time', time_values)]) # Attributes dictionary output_attrs_dict = {} @@ -145,12 +141,7 @@ def update_xrdataset(input_ds, glac_mass_monthly): count_vn = 0 encoding = {} for vn in output_coords_dict.keys(): - empty_holder = np.zeros( - [ - len(output_coords_dict[vn][i]) - for i in list(output_coords_dict[vn].keys()) - ] - ) + empty_holder = np.zeros([len(output_coords_dict[vn][i]) for i in list(output_coords_dict[vn].keys())]) output_ds = xr.Dataset( {vn: (list(output_coords_dict[vn].keys()), empty_holder)}, coords=output_coords_dict[vn], @@ -191,8 +182,7 @@ def run(simpath): # calculate monthly mass - pygem glac_massbaltotal_monthly is in units of m3, so convert to mass using density of ice glac_mass_monthly = get_monthly_mass( statsds.glac_mass_annual.values, - statsds.glac_massbaltotal_monthly.values - * pygem_prms['constants']['density_ice'], + statsds.glac_massbaltotal_monthly.values * pygem_prms['constants']['density_ice'], ) statsds.close() @@ -203,9 +193,7 @@ def run(simpath): statsds.close() # append to existing stats netcdf - output_ds_stats.to_netcdf( - simpath, mode='a', encoding=encoding, engine='netcdf4' - ) + output_ds_stats.to_netcdf(simpath, mode='a', encoding=encoding, engine='netcdf4') # close datasets output_ds_stats.close() diff --git a/pygem/bin/preproc/preproc_fetch_mbdata.py b/pygem/bin/preproc/preproc_fetch_mbdata.py index a10063a7..7a3af64b 100644 --- a/pygem/bin/preproc/preproc_fetch_mbdata.py +++ b/pygem/bin/preproc/preproc_fetch_mbdata.py @@ -53,9 +53,7 @@ def run(fp='', debug=False, overwrite=False): mbdf_subset = mbdf_subset.sort_values(by='rgiid') # rename some keys to work with what other scripts/functions expect - mbdf_subset = mbdf_subset.rename( - columns={'dmdtda': 'mb_mwea', 'err_dmdtda': 'mb_mwea_err'} - ) + mbdf_subset = mbdf_subset.rename(columns={'dmdtda': 'mb_mwea', 'err_dmdtda': 'mb_mwea_err'}) if fp[-4:] != '.csv': fp += '.csv' diff --git a/pygem/bin/preproc/preproc_wgms_estimate_kp.py b/pygem/bin/preproc/preproc_wgms_estimate_kp.py index f9047452..21bc8dd5 100644 --- a/pygem/bin/preproc/preproc_wgms_estimate_kp.py +++ b/pygem/bin/preproc/preproc_wgms_estimate_kp.py @@ -143,51 +143,37 @@ def subset_winter( try: e_idx = np.where( - (wgms_e_df['NAME'] == name) - & (wgms_e_df['WGMS_ID'] == wgmsid) - & (wgms_e_df['Year'] == year) + (wgms_e_df['NAME'] == name) & (wgms_e_df['WGMS_ID'] == wgmsid) & (wgms_e_df['Year'] == year) )[0][0] except: e_idx = None if e_idx is not None: - wgms_ee_df_winter.loc[nrow, wgms_e_cns2add] = wgms_e_df.loc[ - e_idx, wgms_e_cns2add - ] + wgms_ee_df_winter.loc[nrow, wgms_e_cns2add] = wgms_e_df.loc[e_idx, wgms_e_cns2add] wgms_ee_df_winter.to_csv(wgms_ee_winter_fp, index=False) # Export subset of data - wgms_ee_df_winter_subset = wgms_ee_df_winter.loc[ - wgms_ee_df_winter['BEGIN_PERIOD'] > subset_time_value - ] + wgms_ee_df_winter_subset = wgms_ee_df_winter.loc[wgms_ee_df_winter['BEGIN_PERIOD'] > subset_time_value] wgms_ee_df_winter_subset = wgms_ee_df_winter_subset.dropna(subset=['END_WINTER']) wgms_ee_df_winter_subset.to_csv(wgms_ee_winter_fp_subset, index=False) -def est_kp( - wgms_ee_winter_fp_subset='', wgms_ee_winter_fp_kp='', wgms_reg_kp_stats_fp='' -): +def est_kp(wgms_ee_winter_fp_subset='', wgms_ee_winter_fp_kp='', wgms_reg_kp_stats_fp=''): """ This is used to estimate the precipitation factor for the bounds of HH2015_mod """ # Load data - assert os.path.exists(wgms_ee_winter_fp_subset), ( - 'wgms_ee_winter_fn_subset does not exist!' - ) + assert os.path.exists(wgms_ee_winter_fp_subset), 'wgms_ee_winter_fn_subset does not exist!' wgms_df = pd.read_csv(wgms_ee_winter_fp_subset, encoding='unicode_escape') # Process dates - wgms_df.loc[:, 'BEGIN_PERIOD'] = ( - wgms_df.loc[:, 'BEGIN_PERIOD'].values.astype(int).astype(str) - ) + wgms_df.loc[:, 'BEGIN_PERIOD'] = wgms_df.loc[:, 'BEGIN_PERIOD'].values.astype(int).astype(str) wgms_df['BEGIN_YEAR'] = [int(x[0:4]) for x in wgms_df.loc[:, 'BEGIN_PERIOD']] wgms_df['BEGIN_MONTH'] = [int(x[4:6]) for x in list(wgms_df.loc[:, 'BEGIN_PERIOD'])] wgms_df['BEGIN_DAY'] = [int(x[6:]) for x in list(wgms_df.loc[:, 'BEGIN_PERIOD'])] wgms_df['BEGIN_YEARMONTH'] = [x[0:6] for x in list(wgms_df.loc[:, 'BEGIN_PERIOD'])] - wgms_df.loc[:, 'END_WINTER'] = ( - wgms_df.loc[:, 'END_WINTER'].values.astype(int).astype(str) - ) + wgms_df.loc[:, 'END_WINTER'] = wgms_df.loc[:, 'END_WINTER'].values.astype(int).astype(str) wgms_df['END_YEAR'] = [int(x[0:4]) for x in wgms_df.loc[:, 'END_WINTER']] wgms_df['END_MONTH'] = [int(x[4:6]) for x in list(wgms_df.loc[:, 'END_WINTER'])] wgms_df['END_DAY'] = [int(x[6:]) for x in list(wgms_df.loc[:, 'END_WINTER'])] @@ -207,8 +193,7 @@ def est_kp( option_wateryear=pygem_prms['climate']['ref_wateryear'], ) dates_table_yearmo = [ - str(dates_table.loc[x, 'year']) + str(dates_table.loc[x, 'month']).zfill(2) - for x in range(dates_table.shape[0]) + str(dates_table.loc[x, 'year']) + str(dates_table.loc[x, 'month']).zfill(2) for x in range(dates_table.shape[0]) ] # ===== LOAD CLIMATE DATA ===== @@ -216,21 +201,13 @@ def est_kp( gcm = class_climate.GCM(name=pygem_prms['climate']['ref_climate_name']) # Air temperature [degC] - gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table - ) + gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray(gcm.temp_fn, gcm.temp_vn, main_glac_rgi, dates_table) # Precipitation [m] - gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table - ) + gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray(gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table) # Elevation [m asl] - gcm_elev = gcm.importGCMfxnearestneighbor_xarray( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi - ) + gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi) # Lapse rate - gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table - ) + gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray(gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table) # ===== PROCESS THE OBSERVATIONS ====== prec_cn = pygem_prms['climate']['ref_climate_name'] + '_prec' @@ -255,39 +232,17 @@ def est_kp( # - spans more than one month # - positive winter balance (since we don't account for melt) if ( - ( - wgms_df_single.loc[nobs, 'BEGIN_MONTH'] >= 1 - and wgms_df_single.loc[nobs, 'BEGIN_MONTH'] <= 12 - ) - and ( - wgms_df_single.loc[nobs, 'BEGIN_DAY'] >= 1 - and wgms_df_single.loc[nobs, 'BEGIN_DAY'] <= 31 - ) - and ( - wgms_df_single.loc[nobs, 'END_MONTH'] >= 1 - and wgms_df_single.loc[nobs, 'END_MONTH'] <= 12 - ) - and ( - wgms_df_single.loc[nobs, 'END_DAY'] >= 1 - and wgms_df_single.loc[nobs, 'END_DAY'] <= 31 - ) - and ( - wgms_df_single.loc[nobs, 'BEGIN_PERIOD'] - < wgms_df_single.loc[nobs, 'END_WINTER'] - ) - and ( - wgms_df_single.loc[nobs, 'BEGIN_YEARMONTH'] - != wgms_df_single.loc[nobs, 'END_YEARMONTH'] - ) + (wgms_df_single.loc[nobs, 'BEGIN_MONTH'] >= 1 and wgms_df_single.loc[nobs, 'BEGIN_MONTH'] <= 12) + and (wgms_df_single.loc[nobs, 'BEGIN_DAY'] >= 1 and wgms_df_single.loc[nobs, 'BEGIN_DAY'] <= 31) + and (wgms_df_single.loc[nobs, 'END_MONTH'] >= 1 and wgms_df_single.loc[nobs, 'END_MONTH'] <= 12) + and (wgms_df_single.loc[nobs, 'END_DAY'] >= 1 and wgms_df_single.loc[nobs, 'END_DAY'] <= 31) + and (wgms_df_single.loc[nobs, 'BEGIN_PERIOD'] < wgms_df_single.loc[nobs, 'END_WINTER']) + and (wgms_df_single.loc[nobs, 'BEGIN_YEARMONTH'] != wgms_df_single.loc[nobs, 'END_YEARMONTH']) and (wgms_df_single.loc[nobs, 'WINTER_BALANCE'] > 0) ): # Begin index - idx_begin = dates_table_yearmo.index( - wgms_df_single.loc[nobs, 'BEGIN_YEARMONTH'] - ) - idx_end = dates_table_yearmo.index( - wgms_df_single.loc[nobs, 'END_YEARMONTH'] - ) + idx_begin = dates_table_yearmo.index(wgms_df_single.loc[nobs, 'BEGIN_YEARMONTH']) + idx_end = dates_table_yearmo.index(wgms_df_single.loc[nobs, 'END_YEARMONTH']) # Fraction of the months to remove remove_prec_begin = ( @@ -296,35 +251,24 @@ def est_kp( / dates_table.loc[idx_begin, 'daysinmonth'] ) remove_prec_end = gcm_prec[glac, idx_end] * ( - 1 - - wgms_df_single.loc[nobs, 'END_DAY'] - / dates_table.loc[idx_end, 'daysinmonth'] + 1 - wgms_df_single.loc[nobs, 'END_DAY'] / dates_table.loc[idx_end, 'daysinmonth'] ) # Winter Precipitation - gcm_prec_winter = ( - gcm_prec[glac, idx_begin : idx_end + 1].sum() - - remove_prec_begin - - remove_prec_end - ) + gcm_prec_winter = gcm_prec[glac, idx_begin : idx_end + 1].sum() - remove_prec_begin - remove_prec_end wgms_df_single.loc[nobs, prec_cn] = gcm_prec_winter # Number of days ndays = ( dates_table.loc[idx_begin:idx_end, 'daysinmonth'].sum() - wgms_df_single.loc[nobs, 'BEGIN_DAY'] - - ( - dates_table.loc[idx_end, 'daysinmonth'] - - wgms_df_single.loc[nobs, 'END_DAY'] - ) + - (dates_table.loc[idx_end, 'daysinmonth'] - wgms_df_single.loc[nobs, 'END_DAY']) ) wgms_df_single.loc[nobs, 'ndays'] = ndays # Estimate precipitation factors # - assumes no melt and all snow (hence a convservative/underestimated estimate) - wgms_df_single['kp'] = ( - wgms_df_single['WINTER_BALANCE'] / 1000 / wgms_df_single[prec_cn] - ) + wgms_df_single['kp'] = wgms_df_single['WINTER_BALANCE'] / 1000 / wgms_df_single[prec_cn] # Record precipitation, precipitation factors, and number of days in main dataframe wgms_df.loc[glac_idx, prec_cn] = wgms_df_single[prec_cn].values @@ -338,9 +282,7 @@ def est_kp( wgms_df_wkp.to_csv(wgms_ee_winter_fp_kp, index=False) # Calculate stats for all and each region - wgms_df_wkp['reg'] = [ - x.split('-')[1].split('.')[0] for x in wgms_df_wkp['rgiid'].values - ] + wgms_df_wkp['reg'] = [x.split('-')[1].split('.')[0] for x in wgms_df_wkp['rgiid'].values] reg_unique = list(wgms_df_wkp['reg'].unique()) # Output dataframe @@ -355,9 +297,7 @@ def est_kp( 'kp_min', 'kp_max', ] - reg_kp_df = pd.DataFrame( - np.zeros((len(reg_unique) + 1, len(reg_kp_cns))), columns=reg_kp_cns - ) + reg_kp_df = pd.DataFrame(np.zeros((len(reg_unique) + 1, len(reg_kp_cns))), columns=reg_kp_cns) # Only those with at least 1 month of data wgms_df_wkp = wgms_df_wkp.loc[wgms_df_wkp['ndays'] >= 30] @@ -369,9 +309,7 @@ def est_kp( reg_kp_df.loc[0, 'kp_mean'] = np.mean(wgms_df_wkp.kp.values) reg_kp_df.loc[0, 'kp_std'] = np.std(wgms_df_wkp.kp.values) reg_kp_df.loc[0, 'kp_med'] = np.median(wgms_df_wkp.kp.values) - reg_kp_df.loc[0, 'kp_nmad'] = median_abs_deviation( - wgms_df_wkp.kp.values, scale='normal' - ) + reg_kp_df.loc[0, 'kp_nmad'] = median_abs_deviation(wgms_df_wkp.kp.values, scale='normal') reg_kp_df.loc[0, 'kp_min'] = np.min(wgms_df_wkp.kp.values) reg_kp_df.loc[0, 'kp_max'] = np.max(wgms_df_wkp.kp.values) @@ -381,15 +319,11 @@ def est_kp( reg_kp_df.loc[nreg + 1, 'region'] = reg reg_kp_df.loc[nreg + 1, 'count_obs'] = wgms_df_wkp_reg.shape[0] - reg_kp_df.loc[nreg + 1, 'count_glaciers'] = len( - wgms_df_wkp_reg['rgiid'].unique() - ) + reg_kp_df.loc[nreg + 1, 'count_glaciers'] = len(wgms_df_wkp_reg['rgiid'].unique()) reg_kp_df.loc[nreg + 1, 'kp_mean'] = np.mean(wgms_df_wkp_reg.kp.values) reg_kp_df.loc[nreg + 1, 'kp_std'] = np.std(wgms_df_wkp_reg.kp.values) reg_kp_df.loc[nreg + 1, 'kp_med'] = np.median(wgms_df_wkp_reg.kp.values) - reg_kp_df.loc[nreg + 1, 'kp_nmad'] = median_abs_deviation( - wgms_df_wkp_reg.kp.values, scale='normal' - ) + reg_kp_df.loc[nreg + 1, 'kp_nmad'] = median_abs_deviation(wgms_df_wkp_reg.kp.values, scale='normal') reg_kp_df.loc[nreg + 1, 'kp_min'] = np.min(wgms_df_wkp_reg.kp.values) reg_kp_df.loc[nreg + 1, 'kp_max'] = np.max(wgms_df_wkp_reg.kp.values) @@ -399,9 +333,7 @@ def est_kp( print(' mean:', np.mean(wgms_df_wkp_reg.kp.values)) print(' std :', np.std(wgms_df_wkp_reg.kp.values)) print(' med :', np.median(wgms_df_wkp_reg.kp.values)) - print( - ' nmad:', median_abs_deviation(wgms_df_wkp_reg.kp.values, scale='normal') - ) + print(' nmad:', median_abs_deviation(wgms_df_wkp_reg.kp.values, scale='normal')) print(' min :', np.min(wgms_df_wkp_reg.kp.values)) print(' max :', np.max(wgms_df_wkp_reg.kp.values)) @@ -409,12 +341,8 @@ def est_kp( def main(): - parser = argparse.ArgumentParser( - description='estimate precipitation factors from WGMS winter mass balance data' - ) - parser.add_argument( - '-o', '--overwrite', action='store_true', help='Flag to overwrite existing data' - ) + parser = argparse.ArgumentParser(description='estimate precipitation factors from WGMS winter mass balance data') + parser.add_argument('-o', '--overwrite', action='store_true', help='Flag to overwrite existing data') args = parser.parse_args() # ===== WGMS DATA ===== # these are hardcoded for the format downloaded from WGMS for their 2020-08 dataset, would need to be updated for newer data @@ -428,9 +356,7 @@ def main(): in_fps = [x for x in [wgms_eee_fp, wgms_ee_fp, wgms_e_fp, wgms_id_fp]] # outputs - wgms_ee_winter_fp = ( - wgms_fp + 'WGMS-FoG-2019-12-EE-MASS-BALANCE-winter_processed.csv' - ) + wgms_ee_winter_fp = wgms_fp + 'WGMS-FoG-2019-12-EE-MASS-BALANCE-winter_processed.csv' wgms_ee_winter_fp_subset = wgms_ee_winter_fp.replace('.csv', '-subset.csv') wgms_ee_winter_fp_kp = wgms_ee_winter_fp.replace('.csv', '-subset-kp.csv') wgms_reg_kp_stats_fp = wgms_fp + 'WGMS-FoG-2019-12-reg_kp_summary.csv' @@ -460,10 +386,7 @@ def main(): subset_time_value=subset_time_value, ) - if ( - not all(os.path.exists(filepath) for filepath in output_kp_fps) - or args.overwrite - ): + if not all(os.path.exists(filepath) for filepath in output_kp_fps) or args.overwrite: est_kp( wgms_ee_winter_fp_subset=wgms_ee_winter_fp_subset, wgms_ee_winter_fp_kp=wgms_ee_winter_fp_kp, diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index 036a5c16..180a458d 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -16,6 +16,7 @@ import os import pickle import time +import warnings from datetime import timedelta import gpytorch @@ -37,16 +38,17 @@ # read the config pygem_prms = config_manager.read_config() +from oggm import cfg, tasks, utils +from oggm.core import flowline + import pygem.oggm_compat as oggm_compat import pygem.pygem_modelsetup as modelsetup from pygem import class_climate, mcmc from pygem.massbalance import PyGEMMassBalance +from pygem.plot import graphics +from pygem.utils._funcs import interp1d_fill_gaps from pygem.utils.stats import mcmc_stats -# from oggm.core import climate -# from oggm.core.flowline import FluxBasedModel -# from oggm.core.inversion import calving_flux_from_depth - # %% FUNCTIONS def getparser(): @@ -163,6 +165,13 @@ def getparser(): default=pygem_prms['calib']['MCMC_params']['mcmc_burn_pct'], help='burn-in percentage for MCMC calibration', ) + parser.add_argument( + '-thin', + action='store', + type=int, + default=pygem_prms['calib']['MCMC_params']['thin_interval'], + help='thinning factor for MCMC calibration', + ) # flags parser.add_argument( @@ -171,13 +180,17 @@ def getparser(): help='Flag to keep glacier lists ordered (default is false)', ) parser.add_argument( - '-spinup', + '-option_calib_elev_change_1d', action='store_true', - help='Flag to use spinup flowlines (default is false)', + default=pygem_prms['calib']['MCMC_params']['option_calib_elev_change_1d'], + help='Flag to calibrate against 1D elevation change data (default is false)', ) parser.add_argument( - '-p', '--progress_bar', action='store_true', help='Flag to show progress bar' + '-spinup', + action='store_true', + help='Flag to use spinup flowlines (default is false)', ) + parser.add_argument('-p', '--progress_bar', action='store_true', help='Flag to show progress bar') parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') return parser @@ -213,9 +226,7 @@ def mb_mwea_calc( mass balance [m w.e. a-1] """ # RUN MASS BALANCE MODEL - mbmod = PyGEMMassBalance( - gdir, modelprms, glacier_rgi_table, fls=fls, option_areaconstant=True - ) + mbmod = PyGEMMassBalance(gdir, modelprms, glacier_rgi_table, fls=fls, option_areaconstant=True) for year in gdir.dates_table.year.unique(): mbmod.get_annual_mb(fls[0].surface_h, fls=fls, fl_id=0, year=year) @@ -229,11 +240,7 @@ def mb_mwea_calc( t1_idx = gdir.mbdata['t1_idx'] t2_idx = gdir.mbdata['t2_idx'] nyears = gdir.mbdata['nyears'] - mb_mwea = ( - mbmod.glac_wide_massbaltotal[t1_idx : t2_idx + 1].sum() - / mbmod.glac_wide_area_annual[0] - / nyears - ) + mb_mwea = mbmod.glac_wide_massbaltotal[t1_idx : t2_idx + 1].sum() / mbmod.glac_wide_area_annual[0] / nyears return nbinyears_negmbclim, mb_mwea # Otherwise return specific mass balance else: @@ -241,14 +248,145 @@ def mb_mwea_calc( t1_idx = gdir.mbdata['t1_idx'] t2_idx = gdir.mbdata['t2_idx'] nyears = gdir.mbdata['nyears'] - mb_mwea = ( - mbmod.glac_wide_massbaltotal[t1_idx : t2_idx + 1].sum() - / mbmod.glac_wide_area_annual[0] - / nyears - ) + mb_mwea = mbmod.glac_wide_massbaltotal[t1_idx : t2_idx + 1].sum() / mbmod.glac_wide_area_annual[0] / nyears return mb_mwea +def calculate_elev_change_1d( + gdir, + modelprms, + glacier_rgi_table, + fls, + debug=False, +): + """ + For a given set of model parameters, run the ice thickness inversion and mass balance model to get binned annual ice thickness change + Convert to monthly thickness by assuming that the flux divergence is constant throughout the year + """ + y0 = gdir.dates_table.year.min() + y1 = gdir.dates_table.year.max() + + # Check that water level is within given bounds + cls = gdir.read_pickle('inversion_input')[-1] + th = cls['hgt'][-1] + vmin, vmax = cfg.PARAMS['free_board_marine_terminating'] + water_level = utils.clip_scalar(0, th - vmax, th - vmin) + # mass balance model with evolving area + mbmod = PyGEMMassBalance(gdir, modelprms, glacier_rgi_table, fls=fls) + # glacier dynamics model + if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: + ev_model = flowline.FluxBasedModel( + fls, + y0=y0, + mb_model=mbmod, + glen_a=gdir.get_diagnostics()['inversion_glen_a'], + fs=gdir.get_diagnostics()['inversion_fs'], + is_tidewater=gdir.is_tidewater, + water_level=water_level, + do_kcalving=pygem_prms['setup']['include_frontalablation'], + ) + else: + ev_model = flowline.SemiImplicitModel( + fls, + y0=y0, + mb_model=mbmod, + glen_a=gdir.get_diagnostics()['inversion_glen_a'], + fs=gdir.get_diagnostics()['inversion_fs'], + is_tidewater=gdir.is_tidewater, + water_level=water_level, + ) + + try: + # run glacier dynamics model forward + diag, ds = ev_model.run_until_and_store(y1 + 1, fl_diag_path=True) + with np.errstate(invalid='ignore'): + # record frontal ablation for tidewater glaciers and update total mass balance + if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: + # glacier-wide frontal ablation (m3 w.e.) + # - note: diag.calving_m3 is cumulative calving, convert to annual calving + calving_m3we_annual = ( + (diag.calving_m3.values[1:] - diag.calving_m3.values[0:-1]) + * pygem_prms['constants']['density_ice'] + / pygem_prms['constants']['density_water'] + ) + # record each year's frontal ablation in m3 w.e. + for n in np.arange(calving_m3we_annual.shape[0]): + ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3we_annual[n] + + # Add mass lost from frontal ablation to Glacier-wide total mass balance (m3 w.e.) + ev_model.mb_model.glac_wide_massbaltotal = ( + ev_model.mb_model.glac_wide_massbaltotal + ev_model.mb_model.glac_wide_frontalablation + ) + + mod_glacierwide_mb_mwea = ( + mbmod.glac_wide_massbaltotal[gdir.mbdata['t1_idx'] : gdir.mbdata['t2_idx'] + 1].sum() + / mbmod.glac_wide_area_annual[0] + / gdir.mbdata['nyears'] + ) + + # if there is an issue evaluating the dynamics model for a given parameter set in MCMC calibration, + # return -inf for mb_mwea and binned_dh, so MCMC calibration won't accept given parameters + except RuntimeError: + return float('-inf'), float('-inf') + + ### get monthly ice thickness + # grab components of interest + thickness_m = ds[0].thickness_m.values.T # glacier thickness [m ice], (nbins, nyears) + + # set any < 0 thickness to nan + thickness_m[thickness_m <= 0] = np.nan + + # climatic mass balance + dotb_monthly = mbmod.glac_bin_massbalclim # climatic mass balance [m w.e.] per month + # convert to m ice + dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) + + ### to get monthly thickness and mass we require monthly flux divergence ### + # we'll assume the flux divergence is constant througohut the year + # ie. take annual values and divide by 12 - use numpy repeat to repeat values across 12 months + flux_div_monthly_mmo = np.repeat(-ds[0].flux_divergence_myr.values.T[:, 1:] / 12, 12, axis=-1) + + # get monthly binned change in thickness + delta_h_monthly = dotb_monthly - flux_div_monthly_mmo # [m ice per month] + + # get binned monthly thickness = running thickness change + initial thickness + running_delta_h_monthly = np.cumsum(delta_h_monthly, axis=-1) + h_monthly = running_delta_h_monthly + thickness_m[:, 0][:, np.newaxis] + + # get surface height at the specified reference year + ref_surface_h = ds[0].bed_h.values + ds[0].thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values + + # aggregate model bin thicknesses as desired + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + h_monthly = np.column_stack( + [ + stats.binned_statistic( + x=ref_surface_h, + values=x, + statistic=np.nanmean, + bins=gdir.elev_change_1d['bin_edges'], + )[0] + for x in h_monthly.T + ] + ) + + # interpolate over any empty bins + h_monthly_ = np.column_stack([interp1d_fill_gaps(x.copy()) for x in h_monthly.T]) + + # difference each set of inds in diff_inds_map + mod_elev_change_1d = np.column_stack( + [ + h_monthly_[:, tup[1]] - h_monthly_[:, tup[0]] + if tup[0] is not None and tup[1] is not None + else np.full(h_monthly_.shape[0], np.nan) + for tup in gdir.elev_change_1d['model2obs_inds_map'] + ] + ) + + return mod_glacierwide_mb_mwea, mod_elev_change_1d + + # class for Gaussian Process model for mass balance emulator class ExactGPModel(gpytorch.models.ExactGP): """Use the simplest form of GP model, exact inference""" @@ -277,9 +415,7 @@ def __init__(self, mod, likelihood, X_mean, X_std, y_mean, y_std): # evaluate the emulator for a given set of model paramaters (note, Xtest should be ordered as so: [tbias, kp, ddfsnow]) def eval(self, Xtest): # normalize each parameter - Xtest[:] = [ - (x - mu) / sigma for x, mu, sigma in zip(Xtest, self.X_mean, self.X_std) - ] + Xtest[:] = [(x - mu) / sigma for x, mu, sigma in zip(Xtest, self.X_mean, self.X_std)] # convert to torch tensor Xtest_normed = torch.tensor(np.array([Xtest])).to(torch.float) # pass to mbEmulator.mod() to evaluate normed values @@ -299,9 +435,7 @@ def load(cls, em_mod_path=None): with open(emulator_extra_fp, 'r') as f: emulator_extra_dict = json.load(f) # convert lists to torch tensors - X_train = torch.stack( - [torch.tensor(lst) for lst in emulator_extra_dict['X_train']], dim=1 - ) + X_train = torch.stack([torch.tensor(lst) for lst in emulator_extra_dict['X_train']], dim=1) X_mean = torch.tensor(emulator_extra_dict['X_mean']) X_std = torch.tensor(emulator_extra_dict['X_std']) y_train = torch.tensor(emulator_extra_dict['y_train']) @@ -383,8 +517,7 @@ def create_emulator( # Split into training and test data and cast to torch tensors X_train, X_test, y_train, y_test = [ - torch.tensor(x).to(torch.float) - for x in sklearn.model_selection.train_test_split(X_norm, y_norm) + torch.tensor(x).to(torch.float) for x in sklearn.model_selection.train_test_split(X_norm, y_norm) ] # Add a small amount of noise y_train += torch.randn(*y_train.shape) * 0.01 @@ -407,9 +540,7 @@ def create_emulator( if debug: f, ax = plt.subplots(1, 1, figsize=(4, 4)) ax.plot(y_test.numpy()[idx], y_pred.mean.numpy()[idx], 'k*') - ax.fill_between( - y_test.numpy()[idx], lower.numpy()[idx], upper.numpy()[idx], alpha=0.5 - ) + ax.fill_between(y_test.numpy()[idx], lower.numpy()[idx], upper.numpy()[idx], alpha=0.5) plt.show() # ----- Find optimal model hyperparameters ----- @@ -417,9 +548,7 @@ def create_emulator( likelihood.train() # Use the adam optimizer - optimizer = torch.optim.Adam( - model.parameters(), lr=0.03 - ) # Includes GaussianLikelihood parameters + optimizer = torch.optim.Adam(model.parameters(), lr=0.03) # Includes GaussianLikelihood parameters # "Loss" for GPs - the marginal log likelihood mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model) @@ -452,9 +581,7 @@ def create_emulator( if debug: f, ax = plt.subplots(1, 1, figsize=(4, 4)) ax.plot(y_test.numpy()[idx], y_pred.mean.numpy()[idx], 'k*') - ax.fill_between( - y_test.numpy()[idx], lower.numpy()[idx], upper.numpy()[idx], alpha=0.5 - ) + ax.fill_between(y_test.numpy()[idx], lower.numpy()[idx], upper.numpy()[idx], alpha=0.5) plt.show() if debug: @@ -466,11 +593,7 @@ def create_emulator( modelprms_set = np.hstack((tbias_set, kp_set, ddf_set)) modelprms_set_norm = (modelprms_set - X_mean) / X_std - y_set_norm = ( - model(torch.tensor(modelprms_set_norm).to(torch.float)) - .mean.detach() - .numpy() - ) + y_set_norm = model(torch.tensor(modelprms_set_norm).to(torch.float)).mean.detach().numpy() y_set = y_set_norm * y_std + y_mean f, ax = plt.subplots(1, 1, figsize=(4, 4)) @@ -542,6 +665,7 @@ def run(list_packed_vars): # Unpack variables glac_no = list_packed_vars[1] ref_climate_name = list_packed_vars[2] + ncores = list_packed_vars[3] parser = getparser() args = parser.parse_args() @@ -559,9 +683,7 @@ def run(list_packed_vars): # ===== LOAD CLIMATE DATA ===== # Climate class - assert ref_climate_name in ['ERA5', 'ERA-Interim'], ( - 'Error: Calibration not set up for ' + ref_climate_name - ) + assert ref_climate_name == 'ERA5', 'Error: Calibration not set up for ' + ref_climate_name gcm = class_climate.GCM(name=ref_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( @@ -578,9 +700,7 @@ def run(list_packed_vars): gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table, verbose=debug ) # Elevation [m asl] - gcm_elev = gcm.importGCMfxnearestneighbor_xarray( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi - ) + gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi) # Lapse rate [degC m-1] (always monthly) gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( gcm.lr_fn, @@ -606,17 +726,12 @@ def run(list_packed_vars): # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== try: - if ( - glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_frontalablation'] - ): + if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: gdir = oggm_compat.single_flowline_glacier_directory(glacier_str) gdir.is_tidewater = False else: # set reset=True to overwrite non-calving directory that may already exist - gdir = oggm_compat.single_flowline_glacier_directory_with_calving( - glacier_str - ) + gdir = oggm_compat.single_flowline_glacier_directory_with_calving(glacier_str) gdir.is_tidewater = True fls = gdir.read_pickle('inversion_flowlines') @@ -634,10 +749,7 @@ def run(list_packed_vars): # ----- Calibration data ----- try: - mbdata_fn = gdir.get_filepath('mb_calib_pygem') - - with open(mbdata_fn, 'r') as f: - gdir.mbdata = json.load(f) + gdir.mbdata = gdir.read_json('mb_calib_pygem') # Tidewater glaciers - use climatic mass balance since calving_k already calibrated separately if gdir.is_tidewater: @@ -654,20 +766,16 @@ def run(list_packed_vars): # Add time indices consistent with dates_table for mb calculations gdir.mbdata['t1_datetime'] = pd.to_datetime(gdir.mbdata['t1_str']) - gdir.mbdata['t2_datetime'] = pd.to_datetime( - gdir.mbdata['t2_str'] - ) - timedelta(days=1) + gdir.mbdata['t2_datetime'] = pd.to_datetime(gdir.mbdata['t2_str']) - timedelta(days=1) t1_year = gdir.mbdata['t1_datetime'].year t1_month = gdir.mbdata['t1_datetime'].month t2_year = gdir.mbdata['t2_datetime'].year t2_month = gdir.mbdata['t2_datetime'].month t1_idx = dates_table[ - (t1_year == dates_table['year']) - & (t1_month == dates_table['month']) + (t1_year == dates_table['year']) & (t1_month == dates_table['month']) ].index.values[0] t2_idx = dates_table[ - (t2_year == dates_table['year']) - & (t2_month == dates_table['month']) + (t2_year == dates_table['year']) & (t2_month == dates_table['month']) ].index.values[0] # Record indices gdir.mbdata['t1_idx'] = t1_idx @@ -685,35 +793,26 @@ def run(list_packed_vars): gdir.mbdata = None # LOG FAILURE - fail_fp = ( - pygem_prms['root'] - + '/Output/cal_fail/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + fail_fp = pygem_prms['root'] + '/Output/cal_fail/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(fail_fp): os.makedirs(fail_fp, exist_ok=True) txt_fn_fail = glacier_str + '-cal_fail.txt' with open(fail_fp + txt_fn_fail, 'w') as text_file: text_file.write(f'Error with mass balance data: {err}') - print( - '\n' - + glacier_str - + ' mass balance data missing. Check dataset and column names.\n' - ) + print('\n' + glacier_str + ' mass balance data missing. Check dataset and column names.\n') except: fls = None - if debug: - assert os.path.exists(mbdata_fn), ( - 'Mass balance data missing. Check dataset and column names' - ) - - # if spinup, grab appropriate flowlines + # if `args.spinup`, grab appropriate model flowlines if args.spinup: fls = oggm_compat.get_spinup_flowlines(gdir, y0=args.ref_startyear) + # if not `args.spinup` and calibrating elevation change, grab model flowlines + elif args.option_calib_elev_change_1d: + if not os.path.exists(gdir.get_filepath('model_flowlines')): + raise FileNotFoundError('No model flowlines found - has inversion been run?') + fls = gdir.read_pickle('model_flowlines') # ----- CALIBRATION OPTIONS ------ if (fls is not None) and (gdir.mbdata is not None) and (glacier_area.sum() > 0): @@ -721,8 +820,7 @@ def run(list_packed_vars): 'kp': pygem_prms['sim']['params']['kp'], 'tbias': pygem_prms['sim']['params']['tbias'], 'ddfsnow': pygem_prms['sim']['params']['ddfsnow'], - 'ddfice': pygem_prms['sim']['params']['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'], + 'ddfice': pygem_prms['sim']['params']['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'], 'tsnow_threshold': pygem_prms['sim']['params']['tsnow_threshold'], 'precgrad': pygem_prms['sim']['params']['precgrad'], } @@ -739,34 +837,22 @@ def run(list_packed_vars): modelprms['tbias'] = tbias_init modelprms['kp'] = kp_init modelprms['ddfsnow'] = ddfsnow_init - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] nsims = pygem_prms['calib']['emulator_params']['emulator_sims'] # Load sims df - sims_fp = ( - pygem_prms['root'] - + '/Output/emulator/sims/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + sims_fp = pygem_prms['root'] + '/Output/emulator/sims/' + glacier_str.split('.')[0].zfill(2) + '/' sims_fn = glacier_str + '-' + str(nsims) + '_emulator_sims.csv' - if ( - not os.path.exists(sims_fp + sims_fn) - or pygem_prms['calib']['emulator_params']['overwrite_em_sims'] - ): + if not os.path.exists(sims_fp + sims_fn) or pygem_prms['calib']['emulator_params']['overwrite_em_sims']: # ----- Temperature bias bounds (ensure reasonable values) ----- # Tbias lower bound based on some bins having negative climatic mass balance tbias_maxacc = ( -1 * ( gdir.historical_climate['temp'] - + gdir.historical_climate['lr'] - * (fls[0].surface_h.min() - gdir.historical_climate['elev']) + + gdir.historical_climate['lr'] * (fls[0].surface_h.min() - gdir.historical_climate['elev']) ).max() ) modelprms['tbias'] = tbias_maxacc @@ -846,8 +932,7 @@ def run(list_packed_vars): modelprms['kp'] = stats.gamma.ppf( 0.99, pygem_prms['calib']['emulator_params']['kp_gamma_alpha'], - scale=1 - / pygem_prms['calib']['emulator_params']['kp_gamma_beta'], + scale=1 / pygem_prms['calib']['emulator_params']['kp_gamma_beta'], ) nbinyears_negmbclim, mb_mwea = mb_mwea_calc( gdir, @@ -955,23 +1040,15 @@ def run(list_packed_vars): # ------ RANDOM RUNS ------- # Temperature bias - if ( - pygem_prms['calib']['emulator_params']['tbias_disttype'] - == 'uniform' - ): - tbias_random = np.random.uniform( - low=tbias_bndlow, high=tbias_bndhigh, size=nsims - ) - elif ( - pygem_prms['calib']['emulator_params']['tbias_disttype'] - == 'truncnormal' - ): - tbias_zlow = (tbias_bndlow - tbias_middle) / pygem_prms[ - 'calib' - ]['emulator_params']['tbias_sigma'] - tbias_zhigh = (tbias_bndhigh - tbias_middle) / pygem_prms[ - 'calib' - ]['emulator_params']['tbias_sigma'] + if pygem_prms['calib']['emulator_params']['tbias_disttype'] == 'uniform': + tbias_random = np.random.uniform(low=tbias_bndlow, high=tbias_bndhigh, size=nsims) + elif pygem_prms['calib']['emulator_params']['tbias_disttype'] == 'truncnormal': + tbias_zlow = (tbias_bndlow - tbias_middle) / pygem_prms['calib']['emulator_params'][ + 'tbias_sigma' + ] + tbias_zhigh = (tbias_bndhigh - tbias_middle) / pygem_prms['calib']['emulator_params'][ + 'tbias_sigma' + ] tbias_random = stats.truncnorm.rvs( a=tbias_zlow, b=tbias_zhigh, @@ -980,15 +1057,12 @@ def run(list_packed_vars): size=nsims, ) if debug: - print( - '\ntbias random:', tbias_random.mean(), tbias_random.std() - ) + print('\ntbias random:', tbias_random.mean(), tbias_random.std()) # Precipitation factor kp_random = stats.gamma.rvs( pygem_prms['calib']['emulator_params']['kp_gamma_alpha'], - scale=1 - / pygem_prms['calib']['emulator_params']['kp_gamma_beta'], + scale=1 / pygem_prms['calib']['emulator_params']['kp_gamma_beta'], size=nsims, ) if debug: @@ -1023,10 +1097,7 @@ def run(list_packed_vars): modelprms['tbias'] = tbias_random[nsim] modelprms['kp'] = kp_random[nsim] modelprms['ddfsnow'] = ddfsnow_random[nsim] - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] nbinyears_negmbclim, mb_mwea = mb_mwea_calc( gdir, modelprms, @@ -1079,19 +1150,12 @@ def run(list_packed_vars): # ----- EMULATOR: Mass balance ----- em_mod_fn = glacier_str + '-emulator-mb_mwea.pth' - em_mod_fp = ( - pygem_prms['root'] - + '/Output/emulator/models/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + em_mod_fp = pygem_prms['root'] + '/Output/emulator/models/' + glacier_str.split('.')[0].zfill(2) + '/' if ( not os.path.exists(em_mod_fp + em_mod_fn) or pygem_prms['calib']['emulator_params']['overwrite_em_sims'] ): - mbEmulator = create_emulator( - glacier_str, sims_df, y_cn='mb_mwea', debug=debug - ) + mbEmulator = create_emulator(glacier_str, sims_df, y_cn='mb_mwea', debug=debug) else: mbEmulator = massbalEmulator.load(em_mod_path=em_mod_fp + em_mod_fn) @@ -1102,9 +1166,7 @@ def run(list_packed_vars): kp_init = pygem_prms['calib']['emulator_params']['kp_init'] kp_bndlow = pygem_prms['calib']['emulator_params']['kp_bndlow'] kp_bndhigh = pygem_prms['calib']['emulator_params']['kp_bndhigh'] - ddfsnow_init = pygem_prms['calib']['emulator_params'][ - 'ddfsnow_init' - ] + ddfsnow_init = pygem_prms['calib']['emulator_params']['ddfsnow_init'] # ----- FUNCTIONS: COMPUTATIONALLY FASTER AND MORE ROBUST THAN SCIPY MINIMIZE ----- def update_bnds( @@ -1161,13 +1223,8 @@ def update_bnds( prm_mid_new = (prm_bndlow_new + prm_bndhigh_new) / 2 modelprms[prm2opt] = prm_mid_new - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_mid_new = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_mid_new = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) if debug: print( @@ -1212,9 +1269,7 @@ def single_param_optimizer( Computationally more robust and sometimes faster than scipy minimize """ - assert prm2opt is not None, ( - 'For single_param_optimizer you must specify parameter to optimize' - ) + assert prm2opt is not None, 'For single_param_optimizer you must specify parameter to optimize' if prm2opt == 'kp': prm_bndlow = kp_bnds[0] @@ -1234,32 +1289,17 @@ def single_param_optimizer( # Lower bound modelprms[prm2opt] = prm_bndlow - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_low = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_low = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) # Upper bound modelprms[prm2opt] = prm_bndhigh - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_high = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_high = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) # Middle bound prm_mid = (prm_bndlow + prm_bndhigh) / 2 modelprms[prm2opt] = prm_mid - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_mid = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_mid = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) if debug: print( @@ -1285,18 +1325,14 @@ def single_param_optimizer( if np.absolute(mb_mwea_low - mb_obs_mwea) <= mb_mwea_threshold: modelprms[prm2opt] = prm_bndlow mb_mwea_mid = mb_mwea_low - elif ( - np.absolute(mb_mwea_low - mb_obs_mwea) <= mb_mwea_threshold - ): + elif np.absolute(mb_mwea_low - mb_obs_mwea) <= mb_mwea_threshold: modelprms[prm2opt] = prm_bndhigh mb_mwea_mid = mb_mwea_high else: ncount = 0 while ( - np.absolute(mb_mwea_mid - mb_obs_mwea) - > mb_mwea_threshold - and np.absolute(mb_mwea_low - mb_mwea_high) - > mb_mwea_threshold + np.absolute(mb_mwea_mid - mb_obs_mwea) > mb_mwea_threshold + and np.absolute(mb_mwea_low - mb_mwea_high) > mb_mwea_threshold ): if debug: print('\n ncount:', ncount) @@ -1354,16 +1390,12 @@ def single_param_optimizer( modelprms['kp'] = kp_bndhigh modelprms['tbias'] = tbias_bndlow modelprms['ddfsnow'] = ddfsnow_init - mb_mwea_bndhigh = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + mb_mwea_bndhigh = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) # Lower bound modelprms['kp'] = kp_bndlow modelprms['tbias'] = tbias_bndhigh modelprms['ddfsnow'] = ddfsnow_init - mb_mwea_bndlow = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + mb_mwea_bndlow = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) if debug: print( 'mb_mwea_max:', @@ -1387,9 +1419,7 @@ def single_param_optimizer( if not os.path.exists(troubleshoot_fp): os.makedirs(troubleshoot_fp, exist_ok=True) txt_fn_extrapfail = glacier_str + '-mbs_obs_outside_bnds.txt' - with open( - troubleshoot_fp + txt_fn_extrapfail, 'w' - ) as text_file: + with open(troubleshoot_fp + txt_fn_extrapfail, 'w') as text_file: text_file.write( glacier_str + ' observed mass balance exceeds max accumulation ' @@ -1413,9 +1443,7 @@ def single_param_optimizer( if not os.path.exists(troubleshoot_fp): os.makedirs(troubleshoot_fp, exist_ok=True) txt_fn_extrapfail = glacier_str + '-mbs_obs_outside_bnds.txt' - with open( - troubleshoot_fp + txt_fn_extrapfail, 'w' - ) as text_file: + with open(troubleshoot_fp + txt_fn_extrapfail, 'w') as text_file: text_file.write( glacier_str + ' observed mass balance below max loss ' @@ -1434,9 +1462,7 @@ def single_param_optimizer( test_count = 0 test_count_acc = 0 - mb_mwea = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + mb_mwea = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) if mb_mwea > mb_obs_mwea: if debug: print('increase tbias, decrease kp') @@ -1565,14 +1591,10 @@ def single_param_optimizer( # Lower bound modelprms['kp'] = kp_bndlow - mb_mwea_kp_low = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + mb_mwea_kp_low = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) # Upper bound modelprms['kp'] = kp_bndhigh - mb_mwea_kp_high = mbEmulator.eval( - [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] - ) + mb_mwea_kp_high = mbEmulator.eval([modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']]) # Optimal precipitation factor if mb_obs_mwea < mb_mwea_kp_low: @@ -1680,10 +1702,7 @@ def single_param_optimizer( modelprms_fn = glacier_str + '-modelprms_dict.json' modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' + pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' ) if not os.path.exists(modelprms_fp): os.makedirs(modelprms_fp, exist_ok=True) @@ -1707,10 +1726,7 @@ def single_param_optimizer( # load emulator em_mod_fn = glacier_str + '-emulator-mb_mwea.pth' em_mod_fp = ( - pygem_prms['root'] - + '/Output/emulator/models/' - + glacier_str.split('.')[0].zfill(2) - + '/' + pygem_prms['root'] + '/Output/emulator/models/' + glacier_str.split('.')[0].zfill(2) + '/' ) assert os.path.exists(em_mod_fp + em_mod_fn), ( f'emulator output does not exist : {em_mod_fp + em_mod_fn}' @@ -1718,9 +1734,7 @@ def single_param_optimizer( mbEmulator = massbalEmulator.load(em_mod_path=em_mod_fp + em_mod_fn) outpath_sfix = '' # output file path suffix if using emulator else: - outpath_sfix = ( - '-fullsim' # output file path suffix if not using emulator - ) + outpath_sfix = '-fullsim' # output file path suffix if not using emulator # --------------------------------- # ----- FUNCTION DECLARATIONS ----- @@ -1739,19 +1753,9 @@ def calc_mb_total_minelev(modelprms): T_minelev = ( glacier_gcm_temp + glacier_gcm_lr - * ( - glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] - - glacier_gcm_elev - ) + * (glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']] - glacier_gcm_elev) + glacier_gcm_lr - * ( - min_elev - - glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] - ) + * (min_elev - glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']]) + modelprms['tbias'] ) # Precipitation using precipitation factor and precipitation gradient @@ -1762,12 +1766,7 @@ def calc_mb_total_minelev(modelprms): * ( 1 + modelprms['precgrad'] - * ( - min_elev - - glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] - ) + * (min_elev - glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']]) ) ) # Accumulation using tsnow_threshold @@ -1777,9 +1776,7 @@ def calc_mb_total_minelev(modelprms): ] # Melt # energy available for melt [degC day] - melt_energy_available = ( - T_minelev * dates_table['daysinmonth'].values - ) + melt_energy_available = T_minelev * dates_table['daysinmonth'].values melt_energy_available[melt_energy_available < 0] = 0 # assume all snow melt because anything more would melt underlying ice in lowermost bin # SNOW MELT [m w.e.] @@ -1794,12 +1791,11 @@ def get_priors(priors): dists = [] for param in ['tbias', 'kp', 'ddfsnow']: if priors[param]['type'] == 'normal': - dist = stats.norm( - loc=priors[param]['mu'], scale=priors[param]['sigma'] - ) + dist = stats.norm(loc=priors[param]['mu'], scale=priors[param]['sigma']) elif priors[param]['type'] == 'uniform': dist = stats.uniform( - low=priors[param]['low'], high=priors[param]['high'] + loc=priors[param]['low'], + scale=priors[param]['high'] - priors[param]['low'], ) elif priors[param]['type'] == 'gamma': dist = stats.gamma( @@ -1808,46 +1804,46 @@ def get_priors(priors): ) elif priors[param]['type'] == 'truncnormal': dist = stats.truncnorm( - a=(priors[param]['low'] - priors[param]['mu']) - / priors[param]['sigma'], - b=(priors[param]['high'] - priors[param]['mu']) - / priors[param]['sigma'], + a=(priors[param]['low'] - priors[param]['mu']) / priors[param]['sigma'], + b=(priors[param]['high'] - priors[param]['mu']) / priors[param]['sigma'], loc=priors[param]['mu'], scale=priors[param]['sigma'], ) dists.append(dist) return dists - def get_initials(dists, threshold=0.01): - # sample priors - ensure that probability of each sample > .01 - initials = None - while initials is None: - # sample from each distribution - xs = [dist.rvs() for dist in dists] - # calculate densities for each sample - ps = [dist.pdf(x) for dist, x in zip(dists, xs)] - - # Check if all densities are greater than the threshold - if all(p > threshold for p in ps): - initials = xs + def get_initials(dists, threshold=0.01, pctl=None): + if pctl: + initials = [dist.ppf(pctl) for dist in dists] + else: + # sample priors - ensure that probability of each sample > .01 + initials = None + while initials is None: + # sample from each distribution + xs = [dist.rvs() for dist in dists] + # calculate densities for each sample + ps = [dist.pdf(x) for dist, x in zip(dists, xs)] + + # Check if all densities are greater than the threshold + if all(p > threshold for p in ps): + initials = xs return initials - def mb_max(*args, **kwargs): - """Model parameters cannot completely melt the glacier (psuedo-likelihood fxn)""" + def mb_max(**kwargs): + """Psuedo-likelihood functionto ensure glacier is not completely melted.""" if kwargs['massbal'] < mb_max_loss: return -np.inf else: return 0 - def must_melt(kp, tbias, ddfsnow, **kwargs): - """Likelihood function for mass balance [mwea] based on model parametersr (psuedo-likelihood fxn)""" + def must_melt(**kwargs): + """Psuedo-likelihood function for mass balance [mwea] based on model parameters.""" modelprms_copy = modelprms.copy() - modelprms_copy['tbias'] = float(tbias) - modelprms_copy['kp'] = float(kp) - modelprms_copy['ddfsnow'] = float(ddfsnow) + modelprms_copy['tbias'] = float(kwargs['tbias']) + modelprms_copy['kp'] = float(kwargs['kp']) + modelprms_copy['ddfsnow'] = float(kwargs['ddfsnow']) modelprms_copy['ddfice'] = ( - modelprms_copy['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] + modelprms_copy['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] ) mb_total_minelev = calc_mb_total_minelev(modelprms_copy) if mb_total_minelev < 0: @@ -1855,6 +1851,17 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): else: return -np.inf + def rho_constraints(**kwargs): + """Psuedo-likelihood function for ablation and accumulation area densities.""" + if 'rhoabl' not in kwargs or 'rhoacc' not in kwargs: + return 0 + rhoabl = float(kwargs['rhoabl']) + rhoacc = float(kwargs['rhoacc']) + if (rhoacc < 0) or (rhoabl < 0) or (rhoacc > rhoabl): + return -np.inf + else: + return 0 + # --------------------------------- # --------------------------------- @@ -1869,10 +1876,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # Mean global ice thickness from Farinotti et al. (2019) used for missing consensus glaciers ice_thickness_constant = 224 consensus_mass = ( - glacier_rgi_table.Area - * 1e6 - * ice_thickness_constant - * pygem_prms['constants']['density_ice'] + glacier_rgi_table.Area * 1e6 * ice_thickness_constant * pygem_prms['constants']['density_ice'] ) mb_max_loss = ( @@ -1891,9 +1895,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): if pygem_prms['calib']['priors_reg_fn'] is not None: # Load priors priors_df = pd.read_csv( - pygem_prms['root'] - + '/Output/calibration/' - + pygem_prms['calib']['priors_reg_fn'] + pygem_prms['root'] + '/Output/calibration/' + pygem_prms['calib']['priors_reg_fn'] ) priors_idx = np.where( (priors_df.O1Region == glacier_rgi_table['O1Region']) @@ -1907,9 +1909,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): tbias_sigma = float(priors_df.loc[priors_idx, 'tbias_std']) else: # Precipitation factor priors - kp_gamma_alpha = pygem_prms['calib']['MCMC_params'][ - 'kp_gamma_alpha' - ] + kp_gamma_alpha = pygem_prms['calib']['MCMC_params']['kp_gamma_alpha'] kp_gamma_beta = pygem_prms['calib']['MCMC_params']['kp_gamma_beta'] # Temperature bias priors tbias_mu = pygem_prms['calib']['MCMC_params']['tbias_mu'] @@ -1921,30 +1921,20 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): 'type': pygem_prms['calib']['MCMC_params']['tbias_disttype'], 'mu': float(tbias_mu), 'sigma': float(tbias_sigma), - 'low': safe_float(getattr(pygem_prms, 'tbias_bndlow', None)), - 'high': safe_float(getattr(pygem_prms, 'tbias_bndhigh', None)), }, 'kp': { 'type': pygem_prms['calib']['MCMC_params']['kp_disttype'], 'alpha': float(kp_gamma_alpha), 'beta': float(kp_gamma_beta), - 'low': safe_float(getattr(pygem_prms, 'kp_bndlow', None)), - 'high': safe_float(getattr(pygem_prms, 'kp_bndhigh', None)), }, 'ddfsnow': { 'type': pygem_prms['calib']['MCMC_params']['ddfsnow_disttype'], 'mu': pygem_prms['calib']['MCMC_params']['ddfsnow_mu'], 'sigma': pygem_prms['calib']['MCMC_params']['ddfsnow_sigma'], - 'low': float( - pygem_prms['calib']['MCMC_params']['ddfsnow_bndlow'] - ), - 'high': float( - pygem_prms['calib']['MCMC_params']['ddfsnow_bndhigh'] - ), + 'low': float(pygem_prms['calib']['MCMC_params']['ddfsnow_bndlow']), + 'high': float(pygem_prms['calib']['MCMC_params']['ddfsnow_bndhigh']), }, } - # define distributions from priors for sampling initials - prior_dists = get_priors(priors) # ------------------ # ----------------------------------- @@ -1953,12 +1943,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # note, temperature bias bounds will remain constant across chains if using emulator if pygem_prms['calib']['MCMC_params']['option_use_emulator']: # Selects from emulator sims dataframe - sims_fp = ( - pygem_prms['root'] - + '/Output/emulator/sims/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + sims_fp = pygem_prms['root'] + '/Output/emulator/sims/' + glacier_str.split('.')[0].zfill(2) + '/' sims_fn = ( glacier_str + '-' @@ -1976,9 +1961,66 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # ------------------- # mass balance observation and standard deviation obs = [(torch.tensor([mb_obs_mwea]), torch.tensor([mb_obs_mwea_err]))] + + # if running full model (no emulator), or calibrating against binned elevation change, several arguments are needed + if args.option_calib_elev_change_1d: + # add density priors if calibrating against binned elevation change + priors['rhoabl'] = { + 'type': pygem_prms['calib']['MCMC_params']['rhoabl_disttype'], + 'mu': float(pygem_prms['calib']['MCMC_params']['rhoabl_mu']), + 'sigma': float(pygem_prms['calib']['MCMC_params']['rhoabl_sigma']), + } + priors['rhoacc'] = { + 'type': pygem_prms['calib']['MCMC_params']['rhoaccum_disttype'], + 'mu': float(pygem_prms['calib']['MCMC_params']['rhoaccum_mu']), + 'sigma': float(pygem_prms['calib']['MCMC_params']['rhoaccum_sigma']), + } + # load binned elev change obs to glacier directory + gdir.elev_change_1d = gdir.read_json('elev_change_1d') + # stack dh and dh_sigma + gdir.elev_change_1d['dh'] = np.column_stack(gdir.elev_change_1d['dh']) + gdir.elev_change_1d['dh_sigma'] = ( + np.column_stack(gdir.elev_change_1d['dh_sigma']) + if not isinstance(gdir.elev_change_1d['dh_sigma'], int) + else gdir.elev_change_1d['dh_sigma'] + ) + # get observation period indices in model date_table + # create lookup dict (timestamp → index) + date_to_index = {d: i for i, d in enumerate(gdir.dates_table['date'])} + gdir.elev_change_1d['model2obs_inds_map'] = [ + ( + date_to_index.get(pd.to_datetime(start)), + date_to_index.get(pd.to_datetime(end)), + ) + for start, end in gdir.elev_change_1d['dates'] + ] + # model equilibrium line elevation for breakpoint of accumulation and ablation area density scaling + gdir.ela = tasks.compute_ela( + gdir, + years=np.arange( + gdir.dates_table.year.min(), + min(2019, gdir.dates_table.year.max() + 1), + ), + ) + + # calculate inds of data v. model + mbfxn = calculate_elev_change_1d # returns (mb_mwea, binned_dm) + mbargs = ( + gdir, # arguments for get_binned_dh() + modelprms, + glacier_rgi_table, + fls, + ) + # append deltah obs and and sigma obs list + obs.append( + ( + torch.tensor(gdir.elev_change_1d['dh']), + torch.tensor(gdir.elev_change_1d['dh_sigma']), + ) + ) # if there are more observations to calibrate against, simply append a tuple of (obs, variance) to obs list # e.g. obs.append((torch.tensor(dmda_array),torch.tensor(dmda_err_array))) - if pygem_prms['calib']['MCMC_params']['option_use_emulator']: + elif pygem_prms['calib']['MCMC_params']['option_use_emulator']: mbfxn = mbEmulator.eval # returns (mb_mwea) mbargs = None # no additional arguments for mbEmulator.eval() else: @@ -1992,22 +2034,37 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # instantiate mbPosterior given priors, and observed values # note, mbEmulator.eval expects the modelprms to be ordered like so: [tbias, kp, ddfsnow], so priors and initial guesses must also be ordered as such) - priors = { - key: priors[key] - for key in ['tbias', 'kp', 'ddfsnow'] - if key in priors - } + priors = {key: priors[key] for key in ['tbias', 'kp', 'ddfsnow', 'rhoabl', 'rhoacc'] if key in priors} mb = mcmc.mbPosterior( obs, priors, mb_func=mbfxn, mb_args=mbargs, - potential_fxns=[mb_max, must_melt], + potential_fxns=[mb_max, must_melt, rho_constraints], + ela=gdir.ela.min() if hasattr(gdir, 'ela') else None, + bin_z=gdir.elev_change_1d['bin_centers'] if hasattr(gdir, 'elev_change_1d') else None, ) - # prepare export modelprms dictionary modelprms_export = {} - for k in ['tbias', 'kp', 'ddfsnow', 'ddfice', 'mb_mwea', 'ar']: + # store model parameters and priors + modelprms_export['precgrad'] = [pygem_prms['sim']['params']['precgrad']] + modelprms_export['tsnow_threshold'] = [pygem_prms['sim']['params']['tsnow_threshold']] + modelprms_export['mb_obs_mwea'] = [float(mb_obs_mwea)] + modelprms_export['mb_obs_mwea_err'] = [float(mb_obs_mwea_err)] + # mcmc keys + ks = ['tbias', 'kp', 'ddfsnow', 'ddfice', 'mb_mwea', 'ar'] + if args.option_calib_elev_change_1d: + modelprms_export['elev_change_1d'] = {} + modelprms_export['elev_change_1d']['bin_edges'] = gdir.elev_change_1d['bin_edges'] + modelprms_export['elev_change_1d']['obs'] = [ob.flatten().tolist() for ob in obs[1]] + modelprms_export['elev_change_1d']['dates'] = [ + (dt1, dt2) for dt1, dt2 in gdir.elev_change_1d['dates'] + ] + ks += ['rhoabl', 'rhoacc'] + modelprms_export['priors'] = priors + + # create nested dictionary for each mcmc key + for k in ks: modelprms_export[k] = {} # ------------------- @@ -2016,71 +2073,85 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # -------------------- try: ### loop over chains, adjust initial guesses accordingly. done in a while loop as to repeat a chain up to one time if it remained stuck throughout ### + attempts_per_chain = 2 # number of repeats per chain (each with different initial guesses) n_chain = 0 - repeat = False while n_chain < args.nchains: - # compile initial guesses and standardize by standard deviations - # for 0th chain, take mean from regional priors - if n_chain == 0: - initial_guesses = torch.tensor( - ( - tbias_mu, - kp_gamma_alpha / kp_gamma_beta, - pygem_prms['calib']['MCMC_params']['ddfsnow_mu'], + n_attempts = 0 + chain_completed = False + while not chain_completed and n_attempts < attempts_per_chain: + # Select initial guesses + if n_chain == 0 and n_attempts == 0: + initial_guesses = torch.tensor( + ( + tbias_mu, + kp_gamma_alpha / kp_gamma_beta, + pygem_prms['calib']['MCMC_params']['ddfsnow_mu'], + ) ) - ) - # for all chains > 0, randomly sample from regional priors - else: - initial_guesses = torch.tensor(get_initials(prior_dists)) - if debug: - print( - f'{glacier_str} chain {n_chain} initials:\ttbias: {initial_guesses[0]:.2f}, kp: {initial_guesses[1]:.2f}, ddfsnow: {initial_guesses[2]:.4f}' - ) - initial_guesses_z = mcmc.z_normalize( - initial_guesses, mb.means, mb.stds - ) + if args.option_calib_elev_change_1d: + initial_guesses = torch.cat( + ( + initial_guesses, + torch.tensor( + [ + float(pygem_prms['calib']['MCMC_params']['rhoabl_mu']), + float(pygem_prms['calib']['MCMC_params']['rhoaccum_mu']), + ] + ), + ) + ) + else: + initial_guesses = torch.tensor(get_initials(get_priors(priors))) - # instantiate sampler - sampler = mcmc.Metropolis(mb.means, mb.stds) + if debug: + print( + f'{glacier_str} chain {n_chain} attempt {n_attempts} initials:\n' + f'tbias: {initial_guesses[0]:.2f}, kp: {initial_guesses[1]:.2f}, ddfsnow: {initial_guesses[2]:.4f}' + + ( + f', rhoabl: {initial_guesses[3]:.1f}, rhoacc: {initial_guesses[4]:.1f}' + if args.option_calib_elev_change_1d + else '' + ) + ) - # draw samples - m_chain_z, pred_chain, m_primes_z, pred_primes, _, ar = ( - sampler.sample( - initial_guesses_z, + # instantiate sampler + sampler = mcmc.Metropolis(mb.means, mb.stds) + # draw samples + m_chain, pred_chain, m_primes, pred_primes, _, ar = sampler.sample( + initial_guesses, mb.log_posterior, n_samples=args.chain_length, h=pygem_prms['calib']['MCMC_params']['mcmc_step'], burnin=int(args.burn_pct / 100 * args.chain_length), - thin_factor=pygem_prms['calib']['MCMC_params'][ - 'thin_interval' - ], + thin_factor=args.thin, progress_bar=args.progress_bar, ) - ) - # Check condition at the end - if (m_chain_z[:, 0] == m_chain_z[0, 0]).all(): - if not repeat and n_chain != 0: - repeat = True - continue + # Check if stuck - this simply checks if the first column of the chain (tbias) is constant + if (m_chain[:, 0] == m_chain[0, 0]).all(): + if debug: + print( + f'Chain {n_chain}, attempt {n_attempts}: stuck. Trying a different initial guess.' + ) + n_attempts += 1 + continue # Try a new initial guess + else: + chain_completed = True + break - # inverse z-normalize the samples to original parameter space - m_chain = mcmc.inverse_z_normalize(m_chain_z, mb.means, mb.stds) - m_primes = mcmc.inverse_z_normalize( - m_primes_z, mb.means, mb.stds - ) + if not chain_completed and debug: + print( + f'Chain {n_chain}: failed to produce an unstuck result after {attempts_per_chain} initial guesses.' + ) # concatenate mass balance - m_chain = torch.cat( - (m_chain, torch.tensor(pred_chain[0]).reshape(-1, 1)), dim=1 - ) + m_chain = torch.cat((m_chain, torch.tensor(pred_chain[0]).reshape(-1, 1)), dim=1) m_primes = torch.cat( (m_primes, torch.tensor(pred_primes[0]).reshape(-1, 1)), dim=1, ) if debug: - # print('\nacceptance ratio:', model.step_method_dict[next(iter(model.stochastics))][0].ratio) print( 'mb_mwea_mean:', np.round(torch.mean(m_chain[:, -1]).item(), 3), @@ -2098,28 +2169,44 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): + glacier_str.split('.')[0].zfill(2) + '/fig/' ) + if args.option_calib_elev_change_1d: + fp += 'dh/' os.makedirs(fp, exist_ok=True) - if args.ncores > 1: + if ncores > 1: show = False else: show = True - mcmc.plot_chain( - m_primes, - m_chain, - obs[0], - ar, - glacier_str, - show=show, - fpath=f'{fp}/{glacier_str}-chain{n_chain}.png', - ) - for i in pred_chain.keys(): - mcmc.plot_resid_hist( - obs[i], - pred_chain[i], + try: + graphics.plot_mcmc_chain( + m_primes, + m_chain, + obs[0], + ar, glacier_str, show=show, - fpath=f'{fp}/{glacier_str}-chain{n_chain}-residuals-{i}.png', + fpath=f'{fp}/{glacier_str}-chain{n_chain}.png', ) + for i in pred_chain.keys(): + graphics.plot_resid_histogram( + obs[i], + pred_chain[i], + glacier_str, + show=show, + fpath=f'{fp}/{glacier_str}-chain{n_chain}-residuals-{i}.png', + ) + if i == 1: + graphics.plot_mcmc_elev_change_1d( + pred_chain[1], + fls, + gdir.elev_change_1d, + gdir.ela.min(), + glacier_str, + show=show, + fpath=f'{fp}/{glacier_str}-chain{n_chain}-elev_change_1d.png', + ) + except Exception as e: + if debug: + print(f'Error plotting chain {n_chain}: {e}') # Store data from model to be exported chain_str = 'chain_' + str(n_chain) @@ -2127,38 +2214,29 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): modelprms_export['kp'][chain_str] = m_chain[:, 1].tolist() modelprms_export['ddfsnow'][chain_str] = m_chain[:, 2].tolist() modelprms_export['ddfice'][chain_str] = ( - m_chain[:, 2] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] + m_chain[:, 2] / pygem_prms['sim']['params']['ddfsnow_iceratio'] ).tolist() - modelprms_export['mb_mwea'][chain_str] = m_chain[:, 3].tolist() + modelprms_export['mb_mwea'][chain_str] = m_chain[:, -1].tolist() modelprms_export['ar'][chain_str] = ar + if args.option_calib_elev_change_1d: + modelprms_export['elev_change_1d'][chain_str] = [ + preds.flatten().tolist() for preds in pred_chain[1] + ] + modelprms_export['rhoabl'][chain_str] = m_chain[:, 3].tolist() + modelprms_export['rhoacc'][chain_str] = m_chain[:, 4].tolist() # increment n_chain only if the current iteration was a repeat n_chain += 1 - # Export model parameters - modelprms_export['precgrad'] = [ - pygem_prms['sim']['params']['precgrad'] - ] - modelprms_export['tsnow_threshold'] = [ - pygem_prms['sim']['params']['tsnow_threshold'] - ] - modelprms_export['mb_obs_mwea'] = [float(mb_obs_mwea)] - modelprms_export['mb_obs_mwea_err'] = [float(mb_obs_mwea_err)] - modelprms_export['priors'] = priors - # compute stats on mcmc parameters modelprms_export = mcmc_stats(modelprms_export) modelprms_fn = glacier_str + '-modelprms_dict.json' modelprms_fp = [ - ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + (pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/') ] + if args.option_calib_elev_change_1d: + modelprms_fp[0] += 'dh/' # if not using emulator (running full model), save output in ./calibration/ and ./calibration-fullsim/ if not pygem_prms['calib']['MCMC_params']['option_use_emulator']: modelprms_fp.append( @@ -2191,9 +2269,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): os.makedirs(mcmc_good_fp, exist_ok=True) txt_fn_good = glacier_str + '-mcmc_success.txt' with open(mcmc_good_fp + txt_fn_good, 'w') as text_file: - text_file.write( - glacier_str + ' successfully exported mcmc results' - ) + text_file.write(glacier_str + ' successfully exported mcmc results') except Exception as err: # MCMC LOG FAILURE @@ -2207,9 +2283,7 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): os.makedirs(mcmc_fail_fp, exist_ok=True) txt_fn_fail = glacier_str + '-mcmc_fail.txt' with open(mcmc_fail_fp + txt_fn_fail, 'w') as text_file: - text_file.write( - glacier_str + f' failed to complete MCMC: {err}' - ) + text_file.write(glacier_str + f' failed to complete MCMC: {err}') # -------------------- # %% ===== HUSS AND HOCK (2015) CALIBRATION ===== @@ -2219,24 +2293,15 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): kp_init = float(pygem_prms['calib']['HH2015_params']['kp_init']) kp_bndlow = float(pygem_prms['calib']['HH2015_params']['kp_bndlow']) kp_bndhigh = float(pygem_prms['calib']['HH2015_params']['kp_bndhigh']) - ddfsnow_init = float( - pygem_prms['calib']['HH2015_params']['ddfsnow_init'] - ) - ddfsnow_bndlow = float( - pygem_prms['calib']['HH2015_params']['ddfsnow_bndlow'] - ) - ddfsnow_bndhigh = float( - pygem_prms['calib']['HH2015_params']['ddfsnow_bndhigh'] - ) + ddfsnow_init = float(pygem_prms['calib']['HH2015_params']['ddfsnow_init']) + ddfsnow_bndlow = float(pygem_prms['calib']['HH2015_params']['ddfsnow_bndlow']) + ddfsnow_bndhigh = float(pygem_prms['calib']['HH2015_params']['ddfsnow_bndhigh']) # ----- Initialize model parameters ----- modelprms['tbias'] = tbias_init modelprms['kp'] = kp_init modelprms['ddfsnow'] = ddfsnow_init - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] continue_param_search = True # ----- FUNCTIONS: COMPUTATIONALLY FASTER AND MORE ROBUST THAN SCIPY MINIMIZE ----- @@ -2284,13 +2349,8 @@ def update_bnds( prm_mid_new = (prm_bndlow_new + prm_bndhigh_new) / 2 modelprms[prm2opt] = prm_mid_new - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_mid_new = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_mid_new = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) if debug: print( @@ -2331,9 +2391,7 @@ def single_param_optimizer( mb_mwea_threshold=0.005, debug=False, ): - assert prm2opt is not None, ( - 'For single_param_optimizer you must specify parameter to optimize' - ) + assert prm2opt is not None, 'For single_param_optimizer you must specify parameter to optimize' if prm2opt == 'kp': prm_bndlow = kp_bnds[0] @@ -2353,32 +2411,17 @@ def single_param_optimizer( # Lower bound modelprms[prm2opt] = prm_bndlow - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_low = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_low = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Upper bound modelprms[prm2opt] = prm_bndhigh - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_high = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_high = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Middle bound prm_mid = (prm_bndlow + prm_bndhigh) / 2 modelprms[prm2opt] = prm_mid - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_mid = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_mid = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) if debug: print( @@ -2409,9 +2452,7 @@ def single_param_optimizer( mb_mwea_mid = mb_mwea_high else: ncount = 0 - while ( - np.absolute(mb_mwea_mid - mb_obs_mwea) > mb_mwea_threshold - ): + while np.absolute(mb_mwea_mid - mb_obs_mwea) > mb_mwea_threshold: if debug: print('\n ncount:', ncount) ( @@ -2452,14 +2493,10 @@ def single_param_optimizer( # Lower bound modelprms['kp'] = kp_bndlow - mb_mwea_kp_low = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + mb_mwea_kp_low = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Upper bound modelprms['kp'] = kp_bndhigh - mb_mwea_kp_high = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + mb_mwea_kp_high = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Optimal precipitation factor if mb_obs_mwea < mb_mwea_kp_low: @@ -2489,9 +2526,7 @@ def single_param_optimizer( # Update parameter values modelprms['kp'] = kp_opt if debug: - print( - ' kp:', np.round(kp_opt, 2), 'mb_mwea:', np.round(mb_mwea, 2) - ) + print(' kp:', np.round(kp_opt, 2), 'mb_mwea:', np.round(mb_mwea, 2)) # ===== ROUND 2: DEGREE-DAY FACTOR OF SNOW ====== if continue_param_search: @@ -2499,22 +2534,12 @@ def single_param_optimizer( print('Round 2:') # Lower bound modelprms['ddfsnow'] = ddfsnow_bndlow - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_ddflow = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_ddflow = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Upper bound modelprms['ddfsnow'] = ddfsnow_bndhigh - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) - mb_mwea_ddfhigh = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + mb_mwea_ddfhigh = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) # Optimal degree-day factor of snow if mb_obs_mwea < mb_mwea_ddfhigh: ddfsnow_opt = ddfsnow_bndhigh @@ -2541,10 +2566,7 @@ def single_param_optimizer( continue_param_search = False # Update parameter values modelprms['ddfsnow'] = ddfsnow_opt - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] if debug: print( ' ddfsnow:', @@ -2567,8 +2589,7 @@ def single_param_optimizer( -1 * ( gdir.historical_climate['temp'] - + gdir.historical_climate['lr'] - * (fls[0].surface_h.min() - gdir.historical_climate['elev']) + + gdir.historical_climate['lr'] * (fls[0].surface_h.min() - gdir.historical_climate['elev']) ).max() ) tbias_bndlow = tbias_max_acc @@ -2586,9 +2607,7 @@ def single_param_optimizer( # Upper bound while mb_mwea > mb_obs_mwea and modelprms['tbias'] < 20: modelprms['tbias'] = modelprms['tbias'] + tbias_step - mb_mwea = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + mb_mwea = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) if debug: print( ' tc:', @@ -2643,12 +2662,7 @@ def single_param_optimizer( modelprms['mb_obs_mwea_err'] = [mb_obs_mwea_err] modelprms_fn = glacier_str + '-modelprms_dict.json' - modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + modelprms_fp = pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(modelprms_fp): os.makedirs(modelprms_fp, exist_ok=True) modelprms_fullfn = modelprms_fp + modelprms_fn @@ -2677,10 +2691,7 @@ def single_param_optimizer( modelprms['tbias'] = tbias_init modelprms['kp'] = kp_init modelprms['ddfsnow'] = ddfsnow_init - modelprms['ddfice'] = ( - modelprms['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + modelprms['ddfice'] = modelprms['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] # ----- FUNCTIONS ----- def objective(modelprms_subset): @@ -2726,9 +2737,7 @@ def run_objective( modelprms_opt = minimize( objective, modelprms_init, - method=pygem_prms['calib']['HH2015mod_params'][ - 'method_opt' - ], + method=pygem_prms['calib']['HH2015mod_params']['method_opt'], bounds=modelprms_bnds, options={'ftol': ftol_opt, 'eps': eps_opt}, ) @@ -2750,8 +2759,7 @@ def run_objective( -1 * ( gdir.historical_climate['temp'] - + gdir.historical_climate['lr'] - * (fls[0].surface_h.min() - gdir.historical_climate['elev']) + + gdir.historical_climate['lr'] * (fls[0].surface_h.min() - gdir.historical_climate['elev']) ).max() ) modelprms['tbias'] = tbias_bndlow @@ -2828,9 +2836,7 @@ def run_objective( tbias_bndhigh_opt = modelprms['tbias'] tbias_bndlow_opt = modelprms['tbias'] - tbias_step # Compute mass balance - mb_mwea = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + mb_mwea = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) if debug: print( 'tbias:', @@ -2864,9 +2870,7 @@ def run_objective( tbias_bndlow_opt = modelprms['tbias'] tbias_bndhigh_opt = modelprms['tbias'] + tbias_step # Compute mass balance - mb_mwea = mb_mwea_calc( - gdir, modelprms, glacier_rgi_table, fls=fls - ) + mb_mwea = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls=fls) if debug: print( 'tbias:', @@ -2939,12 +2943,7 @@ def run_objective( modelprms['mb_obs_mwea_err'] = [mb_obs_mwea_err] modelprms_fn = glacier_str + '-modelprms_dict.json' - modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + modelprms_fp = pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(modelprms_fp): os.makedirs(modelprms_fp, exist_ok=True) modelprms_fullfn = modelprms_fp + modelprms_fn @@ -2959,12 +2958,7 @@ def run_objective( else: # LOG FAILURE - fail_fp = ( - pygem_prms['root'] - + '/Outputcal_fail/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + fail_fp = pygem_prms['root'] + '/Outputcal_fail/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(fail_fp): os.makedirs(fail_fp, exist_ok=True) txt_fn_fail = glacier_str + '-cal_fail.txt' @@ -3010,9 +3004,7 @@ def main(): num_cores = 1 # Glacier number lists to pass for parallel processing - glac_no_lsts = modelsetup.split_list( - glac_no, n=num_cores, option_ordered=args.option_ordered - ) + glac_no_lsts = modelsetup.split_list(glac_no, n=num_cores, option_ordered=args.option_ordered) # Read GCM names from argument parser ref_climate_name = args.ref_climate_name @@ -3021,7 +3013,7 @@ def main(): # Pack variables for multiprocessing list_packed_vars = [] for count, glac_no_lst in enumerate(glac_no_lsts): - list_packed_vars.append([count, glac_no_lst, ref_climate_name]) + list_packed_vars.append([count, glac_no_lst, ref_climate_name, num_cores]) # Parallel processing if num_cores > 1: print('Processing in parallel with ' + str(num_cores) + ' cores...') diff --git a/pygem/bin/run/run_calibration_frontalablation.py b/pygem/bin/run/run_calibration_frontalablation.py index 9e347e7b..68c11511 100644 --- a/pygem/bin/run/run_calibration_frontalablation.py +++ b/pygem/bin/run/run_calibration_frontalablation.py @@ -53,9 +53,7 @@ calving_k_bndhigh_gl = 5 calving_k_step_gl = 0.2 -perc_threshold_agreement = ( - 0.05 # Threshold (%) at which to stop optimization and consider good agreement -) +perc_threshold_agreement = 0.05 # Threshold (%) at which to stop optimization and consider good agreement fa_threshold = 1e-4 # Absolute threshold at which to stop optimization (Gta) rgi_reg_dict = { @@ -130,9 +128,7 @@ def reg_calving_flux( # ===== LOAD CLIMATE DATA ===== # Climate class - assert args.ref_climate_name in ['ERA5', 'ERA-Interim'], ( - 'Error: Calibration not set up for ' + args.ref_climate_name - ) + assert args.ref_climate_name == 'ERA5', 'Error: Calibration not set up for ' + args.ref_climate_name gcm = class_climate.GCM(name=args.ref_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( @@ -149,9 +145,7 @@ def reg_calving_flux( gcm.prec_fn, gcm.prec_vn, main_glac_rgi, dates_table, verbose=debug ) # Elevation [m asl] - gcm_elev = gcm.importGCMfxnearestneighbor_xarray( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi - ) + gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi) # Lapse rate [degC m-1] gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, verbose=debug @@ -167,9 +161,7 @@ def reg_calving_flux( 'no_errors', 'oggm_dynamics', ] - output_df = pd.DataFrame( - np.zeros((main_glac_rgi.shape[0], len(output_cns))), columns=output_cns - ) + output_df = pd.DataFrame(np.zeros((main_glac_rgi.shape[0], len(output_cns))), columns=output_cns) output_df['RGIId'] = main_glac_rgi.RGIId output_df['calving_k'] = calving_k output_df['calving_thick'] = np.nan @@ -214,12 +206,7 @@ def reg_calving_flux( # Use the calibrated model parameters (although they were calibrated without accounting for calving) if args.prms_from_glac_cal: modelprms_fn = glacier_str + '-modelprms_dict.json' - modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' - ) + modelprms_fp = pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(modelprms_fp + modelprms_fn): # try using regional priors args.prms_from_reg_priors = True @@ -234,9 +221,7 @@ def reg_calving_flux( if pygem_prms['calib']['priors_reg_fn'] is not None: # Load priors priors_df = pd.read_csv( - pygem_prms['root'] - + '/Output/calibration/' - + pygem_prms['calib']['priors_reg_fn'] + pygem_prms['root'] + '/Output/calibration/' + pygem_prms['calib']['priors_reg_fn'] ) priors_idx = np.where( (priors_df.O1Region == glacier_rgi_table['O1Region']) @@ -250,8 +235,7 @@ def reg_calving_flux( 'kp': kp_value, 'tbias': tbias_value, 'ddfsnow': pygem_prms['sim']['params']['ddfsnow'], - 'ddfice': pygem_prms['sim']['params']['ddfsnow'] - / pygem_prms['sim']['params']['ddfsnow_iceratio'], + 'ddfice': pygem_prms['sim']['params']['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'], 'tsnow_threshold': pygem_prms['sim']['params']['tsnow_threshold'], 'precgrad': pygem_prms['sim']['params']['precgrad'], } @@ -262,32 +246,20 @@ def reg_calving_flux( if pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a']: glena_df = pd.read_csv( - pygem_prms['root'] - + pygem_prms['sim']['oggm_dynamics']['glen_a_regional_relpath'] + pygem_prms['root'] + pygem_prms['sim']['oggm_dynamics']['glen_a_regional_relpath'] ) - glena_idx = np.where(glena_df.O1Region == glacier_rgi_table.O1Region)[ - 0 - ][0] + glena_idx = np.where(glena_df.O1Region == glacier_rgi_table.O1Region)[0][0] glen_a_multiplier = glena_df.loc[glena_idx, 'glens_a_multiplier'] fs = glena_df.loc[glena_idx, 'fs'] else: fs = pygem_prms['sim']['oggm_dynamics']['fs'] - glen_a_multiplier = pygem_prms['sim']['oggm_dynamics'][ - 'glen_a_multiplier' - ] + glen_a_multiplier = pygem_prms['sim']['oggm_dynamics']['glen_a_multiplier'] # CFL number (may use different values for calving to prevent errors) - if ( - glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_frontalablation'] - ): - cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics'][ - 'cfl_number' - ] + if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: + cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics']['cfl_number'] else: - cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics'][ - 'cfl_number_calving' - ] + cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics']['cfl_number_calving'] # ----- Mass balance model for ice thickness inversion using OGGM ----- mbmod_inv = PyGEMMassBalance( @@ -312,9 +284,7 @@ def reg_calving_flux( if invert_standard: apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) tasks.prepare_for_inversion(gdir) - tasks.mass_conservation_inversion( - gdir, glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, fs=fs - ) + tasks.mass_conservation_inversion(gdir, glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, fs=fs) else: tasks.find_inversion_calving_from_any_mb( gdir, @@ -325,14 +295,10 @@ def reg_calving_flux( # ------ MODEL WITH EVOLVING AREA ------ tasks.init_present_time_glacier(gdir) # adds bins below - debris.debris_binned( - gdir, fl_str='model_flowlines' - ) # add debris enhancement factors to flowlines + debris.debris_binned(gdir, fl_str='model_flowlines') # add debris enhancement factors to flowlines nfls = gdir.read_pickle('model_flowlines') # Mass balance model - mbmod = PyGEMMassBalance( - gdir, modelprms, glacier_rgi_table, fls=nfls, option_areaconstant=True - ) + mbmod = PyGEMMassBalance(gdir, modelprms, glacier_rgi_table, fls=nfls, option_areaconstant=True) # Water Level # Check that water level is within given bounds cls = gdir.read_pickle('inversion_input')[-1] @@ -367,14 +333,11 @@ def reg_calving_flux( / pygem_prms['constants']['density_water'] ) for n in np.arange(calving_m3_annual.shape[0]): - ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = ( - calving_m3_annual[n] - ) + ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3_annual[n] # Glacier-wide total mass balance (m3 w.e.) ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation + ev_model.mb_model.glac_wide_massbaltotal - ev_model.mb_model.glac_wide_frontalablation ) # if debug: @@ -387,9 +350,7 @@ def reg_calving_flux( # Output of calving out_calving_forward = {} # calving flux (km3 ice/yr) - out_calving_forward['calving_flux'] = ( - calving_m3_annual.sum() / nyears / 1e9 - ) + out_calving_forward['calving_flux'] = calving_m3_annual.sum() / nyears / 1e9 # calving flux (Gt/yr) calving_flux_Gta = ( out_calving_forward['calving_flux'] @@ -404,9 +365,7 @@ def reg_calving_flux( # Record in dataframe output_df.loc[nglac, 'calving_flux_Gta'] = calving_flux_Gta - output_df.loc[nglac, 'calving_thick'] = out_calving_forward[ - 'calving_front_thick' - ] + output_df.loc[nglac, 'calving_thick'] = out_calving_forward['calving_front_thick'] output_df.loc[nglac, 'no_errors'] = 1 output_df.loc[nglac, 'oggm_dynamics'] = 1 @@ -446,13 +405,10 @@ def reg_calving_flux( # Record frontal ablation for tidewater glaciers and update total mass balance # Update glacier-wide frontal ablation (m3 w.e.) - ev_model.mb_model.glac_wide_frontalablation = ( - ev_model.mb_model.glac_bin_frontalablation.sum(0) - ) + ev_model.mb_model.glac_wide_frontalablation = ev_model.mb_model.glac_bin_frontalablation.sum(0) # Update glacier-wide total mass balance (m3 w.e.) ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation + ev_model.mb_model.glac_wide_massbaltotal - ev_model.mb_model.glac_wide_frontalablation ) calving_flux_km3a = ( @@ -486,9 +442,7 @@ def reg_calving_flux( # Record in dataframe output_df.loc[nglac, 'calving_flux_Gta'] = calving_flux_Gta - output_df.loc[nglac, 'calving_thick'] = out_calving_forward[ - 'calving_front_thick' - ] + output_df.loc[nglac, 'calving_thick'] = out_calving_forward['calving_front_thick'] output_df.loc[nglac, 'no_errors'] = 1 if args.verbose or debug: @@ -513,12 +467,9 @@ def reg_calving_flux( last_yr_idx = np.where(mbmod.glac_wide_area_annual > 0)[0][-1] if last_yr_idx == mbmod.glac_bin_area_annual.shape[1] - 1: last_yr_idx = -2 - bin_last_idx = np.where(mbmod.glac_bin_area_annual[:, last_yr_idx] > 0)[ - 0 - ][-1] + bin_last_idx = np.where(mbmod.glac_bin_area_annual[:, last_yr_idx] > 0)[0][-1] bin_area_lost = ( - mbmod.glac_bin_area_annual[bin_last_idx:, 0] - - mbmod.glac_bin_area_annual[bin_last_idx:, -2] + mbmod.glac_bin_area_annual[bin_last_idx:, 0] - mbmod.glac_bin_area_annual[bin_last_idx:, -2] ) height_asl = mbmod.heights - water_level height_asl[mbmod.heights < 0] = 0 @@ -529,9 +480,7 @@ def reg_calving_flux( / pygem_prms['constants']['density_water'] / nyears ) - mb_mwea_fa_asl_geo_correction_max = 0.3 * gta_to_mwea( - calving_flux_Gta, glacier_rgi_table['Area'] * 1e6 - ) + mb_mwea_fa_asl_geo_correction_max = 0.3 * gta_to_mwea(calving_flux_Gta, glacier_rgi_table['Area'] * 1e6) if mb_mwea_fa_asl_geo_correction > mb_mwea_fa_asl_geo_correction_max: mb_mwea_fa_asl_geo_correction = mb_mwea_fa_asl_geo_correction_max @@ -554,9 +503,7 @@ def reg_calving_flux( # print(' mb_mwea_fa_asl_geo_correction:', mb_mwea_fa_asl_geo_correction) # print(glacier_rgi_table, glacier_rgi_table['Area']) - output_df.loc[nglac, 'mb_mwea_fa_asl_lost'] = ( - mb_mwea_fa_asl_geo_correction - ) + output_df.loc[nglac, 'mb_mwea_fa_asl_lost'] = mb_mwea_fa_asl_geo_correction if out_calving_forward is None: output_df.loc[ @@ -575,9 +522,7 @@ def reg_calving_flux( rgiids_data = list(fa_glac_data_reg.RGIId.values) rgiids_mod = list(output_df_good.RGIId.values) fa_data_idx = [rgiids_data.index(x) for x in rgiids_mod] - reg_calving_gta_obs_good = fa_glac_data_reg.loc[ - fa_data_idx, frontal_ablation_Gta_cn - ].sum() + reg_calving_gta_obs_good = fa_glac_data_reg.loc[fa_data_idx, frontal_ablation_Gta_cn].sum() else: reg_calving_gta_mod_good = output_df.calving_flux_Gta.sum() reg_calving_gta_obs_good = fa_glac_data_reg[frontal_ablation_Gta_cn].sum() @@ -633,8 +578,7 @@ def run_opt_fa( and np.round(calving_k, 2) < calving_k_bndhigh and calving_k > calving_k_bndlow and ( - np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs - > perc_threshold_agreement + np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs > perc_threshold_agreement and np.abs(reg_calving_gta_mod - reg_calving_gta_obs) > fa_threshold ) ): @@ -681,10 +625,7 @@ def run_opt_fa( print('calving_k_bndlow:', calving_k_bndlow) print( 'fa perc:', - ( - np.abs(reg_calving_gta_mod - reg_calving_gta_obs) - / reg_calving_gta_obs - ), + (np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs), ) print('fa thres:', np.abs(reg_calving_gta_mod - reg_calving_gta_obs)) print('good values:', output_df.loc[0, 'calving_flux_Gta']) @@ -693,8 +634,7 @@ def run_opt_fa( reg_calving_gta_mod > reg_calving_gta_obs and calving_k > calving_k_bndlow and ( - np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs - > perc_threshold_agreement + np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs > perc_threshold_agreement and np.abs(reg_calving_gta_mod - reg_calving_gta_obs) > fa_threshold ) ) and not np.isnan(output_df.loc[0, 'calving_flux_Gta']): @@ -730,18 +670,14 @@ def run_opt_fa( if args.verbose: print('bnds:', calving_k_bndlow, calving_k_bndhigh) - print( - 'bnds gt/yr:', reg_calving_gta_mod_bndlow, reg_calving_gta_mod_bndhigh - ) + print('bnds gt/yr:', reg_calving_gta_mod_bndlow, reg_calving_gta_mod_bndhigh) # ----- Optimize further using mid-point "bisection" method ----- # Consider replacing with scipy.optimize.brent if not np.isnan(output_df.loc[0, 'calving_flux_Gta']): # Check if upper bound causes good fit if ( - np.abs(reg_calving_gta_mod_bndhigh - reg_calving_gta_obs) - / reg_calving_gta_obs - < perc_threshold_agreement + np.abs(reg_calving_gta_mod_bndhigh - reg_calving_gta_obs) / reg_calving_gta_obs < perc_threshold_agreement or np.abs(reg_calving_gta_mod_bndhigh - reg_calving_gta_obs) < fa_threshold ): # If so, calving_k equals upper bound and re-run to get proper estimates for output @@ -764,9 +700,7 @@ def run_opt_fa( # Check if lower bound causes good fit elif ( - np.abs(reg_calving_gta_mod_bndlow - reg_calving_gta_obs) - / reg_calving_gta_obs - < perc_threshold_agreement + np.abs(reg_calving_gta_mod_bndlow - reg_calving_gta_obs) / reg_calving_gta_obs < perc_threshold_agreement or np.abs(reg_calving_gta_mod_bndlow - reg_calving_gta_obs) < fa_threshold ): calving_k = calving_k_bndlow @@ -799,9 +733,7 @@ def run_opt_fa( while ( ( - np.abs(reg_calving_gta_mod - reg_calving_gta_obs) - / reg_calving_gta_obs - > perc_threshold_agreement + np.abs(reg_calving_gta_mod - reg_calving_gta_obs) / reg_calving_gta_obs > perc_threshold_agreement and np.abs(reg_calving_gta_mod - reg_calving_gta_obs) > fa_threshold ) and nround <= nround_max @@ -849,7 +781,9 @@ def run_opt_fa( def merge_data(frontalablation_fp='', overwrite=False, verbose=False): - frontalablation_fn1 = 'Northern_hemisphere_calving_flux_Kochtitzky_et_al_for_David_Rounce_with_melt_v14-wromainMB.csv' + frontalablation_fn1 = ( + 'Northern_hemisphere_calving_flux_Kochtitzky_et_al_for_David_Rounce_with_melt_v14-wromainMB.csv' + ) frontalablation_fn2 = 'frontalablation_glacier_data_minowa2021.csv' frontalablation_fn3 = 'frontalablation_glacier_data_osmanoglu.csv' out_fn = frontalablation_fn1.replace('.csv', '-w17_19.csv') @@ -885,12 +819,8 @@ def merge_data(frontalablation_fp='', overwrite=False, verbose=False): columns=fa_glac_data_cns_subset, ) fa_data_df1['RGIId'] = fa_glac_data1['RGIId'] - fa_data_df1['fa_gta_obs'] = fa_glac_data1[ - 'Frontal_ablation_2000_to_2020_gt_per_yr_mean' - ] - fa_data_df1['fa_gta_obs_unc'] = fa_glac_data1[ - 'Frontal_ablation_2000_to_2020_gt_per_yr_mean_err' - ] + fa_data_df1['fa_gta_obs'] = fa_glac_data1['Frontal_ablation_2000_to_2020_gt_per_yr_mean'] + fa_data_df1['fa_gta_obs_unc'] = fa_glac_data1['Frontal_ablation_2000_to_2020_gt_per_yr_mean_err'] fa_data_df1['Romain_gta_mbtot'] = fa_glac_data1['Romain_gta_mbtot'] fa_data_df1['Romain_gta_mbclim'] = fa_glac_data1['Romain_gta_mbclim'] fa_data_df1['Romain_mwea_mbtot'] = fa_glac_data1['Romain_mwea_mbtot'] @@ -933,9 +863,7 @@ def merge_data(frontalablation_fp='', overwrite=False, verbose=False): # Export frontal ablation data for Will fa_data_df.to_csv(frontalablation_fp + out_fn, index=False) if verbose: - print( - f'Combined frontal ablation dataset exported: {frontalablation_fp + out_fn}' - ) + print(f'Combined frontal ablation dataset exported: {frontalablation_fp + out_fn}') return out_fn @@ -952,9 +880,7 @@ def calib_ind_calving_k( # Load calving glacier data fa_glac_data = pd.read_csv(frontalablation_fp + frontalablation_fn) mb_data = pd.read_csv(hugonnet2021_fp) - fa_glac_data['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in fa_glac_data.RGIId.values - ] + fa_glac_data['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in fa_glac_data.RGIId.values] calving_k_bndhigh_set = np.copy(calving_k_bndhigh_gl) calving_k_bndlow_set = np.copy(calving_k_bndlow_gl) @@ -974,10 +900,7 @@ def calib_ind_calving_k( for nglac, rgiid in enumerate(fa_glac_data_reg.RGIId): # Avoid regional data and observations from multiple RGIIds (len==14) - if ( - not fa_glac_data_reg.loc[nglac, 'RGIId'] == 'all' - and len(fa_glac_data_reg.loc[nglac, 'RGIId']) == 14 - ): + if not fa_glac_data_reg.loc[nglac, 'RGIId'] == 'all' and len(fa_glac_data_reg.loc[nglac, 'RGIId']) == 14: fa_glac_data_reg.loc[nglac, 'glacno'] = rgiid[ -8: ] # (str(int(rgiid.split('-')[1].split('.')[0])) + '.' + @@ -994,15 +917,11 @@ def calib_ind_calving_k( main_glac_rgi_all = modelsetup.selectglaciersrgitable(glac_no=glacno_reg_wdata) # Tidewater glaciers termtype_list = [1, 5] - main_glac_rgi = main_glac_rgi_all.loc[ - main_glac_rgi_all['TermType'].isin(termtype_list) - ] + main_glac_rgi = main_glac_rgi_all.loc[main_glac_rgi_all['TermType'].isin(termtype_list)] main_glac_rgi.reset_index(inplace=True, drop=True) # ----- QUALITY CONTROL USING MB_CLIM COMPARED TO REGIONAL MASS BALANCE ----- - mb_data['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in mb_data.rgiid.values - ] + mb_data['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in mb_data.rgiid.values] mb_data_reg = mb_data.loc[mb_data['O1Region'] == reg, :] mb_data_reg.reset_index(inplace=True) @@ -1043,16 +962,12 @@ def calib_ind_calving_k( 'mb_total_mwea', ] - output_df_all = pd.DataFrame( - np.zeros((main_glac_rgi.shape[0], len(output_cns))), columns=output_cns - ) + output_df_all = pd.DataFrame(np.zeros((main_glac_rgi.shape[0], len(output_cns))), columns=output_cns) output_df_all['RGIId'] = main_glac_rgi.RGIId output_df_all['calving_k_nmad'] = 0.0 # Load observations - fa_obs_dict = dict( - zip(fa_glac_data_reg.RGIId, fa_glac_data_reg[frontal_ablation_Gta_cn]) - ) + fa_obs_dict = dict(zip(fa_glac_data_reg.RGIId, fa_glac_data_reg[frontal_ablation_Gta_cn])) fa_obs_unc_dict = dict( zip( fa_glac_data_reg.RGIId, @@ -1063,9 +978,7 @@ def calib_ind_calving_k( rgi_area_dict = dict(zip(main_glac_rgi.RGIId, main_glac_rgi.Area)) output_df_all['fa_gta_obs'] = output_df_all['RGIId'].map(fa_obs_dict) - output_df_all['fa_gta_obs_unc'] = output_df_all['RGIId'].map( - fa_obs_unc_dict - ) + output_df_all['fa_gta_obs_unc'] = output_df_all['RGIId'].map(fa_obs_unc_dict) # output_df_all['name'] = output_df_all['RGIId'].map(fa_glacname_dict) output_df_all['area_km2'] = output_df_all['RGIId'].map(rgi_area_dict) @@ -1079,18 +992,10 @@ def calib_ind_calving_k( # output_df_all['thick_measured_yn'] = np.nan for nglac, rgiid in enumerate(list(output_df_all.RGIId)): fa_idx = fa_rgiids_list.index(rgiid) - output_df_all.loc[nglac, 'mb_total_gta_obs'] = fa_glac_data_reg.loc[ - fa_idx, 'Romain_gta_mbtot' - ] - output_df_all.loc[nglac, 'mb_clim_gta_obs'] = fa_glac_data_reg.loc[ - fa_idx, 'Romain_gta_mbclim' - ] - output_df_all.loc[nglac, 'mb_total_mwea_obs'] = fa_glac_data_reg.loc[ - fa_idx, 'Romain_mwea_mbtot' - ] - output_df_all.loc[nglac, 'mb_clim_mwea_obs'] = fa_glac_data_reg.loc[ - fa_idx, 'Romain_mwea_mbclim' - ] + output_df_all.loc[nglac, 'mb_total_gta_obs'] = fa_glac_data_reg.loc[fa_idx, 'Romain_gta_mbtot'] + output_df_all.loc[nglac, 'mb_clim_gta_obs'] = fa_glac_data_reg.loc[fa_idx, 'Romain_gta_mbclim'] + output_df_all.loc[nglac, 'mb_total_mwea_obs'] = fa_glac_data_reg.loc[fa_idx, 'Romain_mwea_mbtot'] + output_df_all.loc[nglac, 'mb_clim_mwea_obs'] = fa_glac_data_reg.loc[fa_idx, 'Romain_mwea_mbclim'] # output_df_all.loc[nglac, 'thick_measured_yn'] = fa_glac_data_reg.loc[fa_idx, 'thick_measured_yn'] # ----- CORRECT TOO POSITIVE CLIMATIC MASS BALANCES ----- @@ -1100,9 +1005,7 @@ def calib_ind_calving_k( output_df_all['mb_total_mwea'] = output_df_all['mb_total_mwea_obs'] output_df_all['fa_gta_max'] = output_df_all['fa_gta_obs'] - output_df_badmbclim = output_df_all.loc[ - output_df_all.mb_clim_mwea_obs > mb_clim_reg_3std - ] + output_df_badmbclim = output_df_all.loc[output_df_all.mb_clim_mwea_obs > mb_clim_reg_3std] # Correct by using mean + 3std as maximum climatic mass balance if output_df_badmbclim.shape[0] > 0: rgiids_toopos = list(output_df_badmbclim.RGIId) @@ -1138,9 +1041,7 @@ def calib_ind_calving_k( main_glac_rgi_ind.reset_index(inplace=True, drop=True) rgiid_ind = main_glac_rgi_ind.loc[0, 'RGIId'] - fa_glac_data_ind = fa_glac_data_reg.loc[ - fa_glac_data_reg.RGIId == rgiid_ind, : - ] + fa_glac_data_ind = fa_glac_data_reg.loc[fa_glac_data_reg.RGIId == rgiid_ind, :] fa_glac_data_ind.reset_index(inplace=True, drop=True) # Update the data @@ -1184,54 +1085,28 @@ def calib_ind_calving_k( reg_calving_gta_mod_bndlow = None # Record bounds - output_df_all.loc[nglac, 'calving_flux_Gta_bndlow'] = ( - reg_calving_gta_mod_bndlow - ) - output_df_all.loc[nglac, 'calving_flux_Gta_bndhigh'] = ( - reg_calving_gta_mod_bndhigh - ) + output_df_all.loc[nglac, 'calving_flux_Gta_bndlow'] = reg_calving_gta_mod_bndlow + output_df_all.loc[nglac, 'calving_flux_Gta_bndhigh'] = reg_calving_gta_mod_bndhigh if verbose: print(' fa_data [Gt/yr]:', np.round(reg_calving_gta_obs, 4)) print(' fa_model_bndlow [Gt/yr] :', reg_calving_gta_mod_bndlow) - print( - ' fa_model_bndhigh [Gt/yr] :', reg_calving_gta_mod_bndhigh - ) + print(' fa_model_bndhigh [Gt/yr] :', reg_calving_gta_mod_bndhigh) run_opt = False if bndhigh_good and bndlow_good: if reg_calving_gta_obs < reg_calving_gta_mod_bndlow: - output_df_all.loc[nglac, 'calving_k'] = ( - output_df_bndlow.loc[0, 'calving_k'] - ) - output_df_all.loc[nglac, 'calving_thick'] = ( - output_df_bndlow.loc[0, 'calving_thick'] - ) - output_df_all.loc[nglac, 'calving_flux_Gta'] = ( - output_df_bndlow.loc[0, 'calving_flux_Gta'] - ) - output_df_all.loc[nglac, 'no_errors'] = ( - output_df_bndlow.loc[0, 'no_errors'] - ) - output_df_all.loc[nglac, 'oggm_dynamics'] = ( - output_df_bndlow.loc[0, 'oggm_dynamics'] - ) + output_df_all.loc[nglac, 'calving_k'] = output_df_bndlow.loc[0, 'calving_k'] + output_df_all.loc[nglac, 'calving_thick'] = output_df_bndlow.loc[0, 'calving_thick'] + output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df_bndlow.loc[0, 'calving_flux_Gta'] + output_df_all.loc[nglac, 'no_errors'] = output_df_bndlow.loc[0, 'no_errors'] + output_df_all.loc[nglac, 'oggm_dynamics'] = output_df_bndlow.loc[0, 'oggm_dynamics'] elif reg_calving_gta_obs > reg_calving_gta_mod_bndhigh: - output_df_all.loc[nglac, 'calving_k'] = ( - output_df_bndhigh.loc[0, 'calving_k'] - ) - output_df_all.loc[nglac, 'calving_thick'] = ( - output_df_bndhigh.loc[0, 'calving_thick'] - ) - output_df_all.loc[nglac, 'calving_flux_Gta'] = ( - output_df_bndhigh.loc[0, 'calving_flux_Gta'] - ) - output_df_all.loc[nglac, 'no_errors'] = ( - output_df_bndhigh.loc[0, 'no_errors'] - ) - output_df_all.loc[nglac, 'oggm_dynamics'] = ( - output_df_bndhigh.loc[0, 'oggm_dynamics'] - ) + output_df_all.loc[nglac, 'calving_k'] = output_df_bndhigh.loc[0, 'calving_k'] + output_df_all.loc[nglac, 'calving_thick'] = output_df_bndhigh.loc[0, 'calving_thick'] + output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df_bndhigh.loc[0, 'calving_flux_Gta'] + output_df_all.loc[nglac, 'no_errors'] = output_df_bndhigh.loc[0, 'no_errors'] + output_df_all.loc[nglac, 'oggm_dynamics'] = output_df_bndhigh.loc[0, 'oggm_dynamics'] else: run_opt = True else: @@ -1248,21 +1123,11 @@ def calib_ind_calving_k( ignore_nan=False, ) calving_k_med = np.copy(calving_k) - output_df_all.loc[nglac, 'calving_k'] = output_df.loc[ - 0, 'calving_k' - ] - output_df_all.loc[nglac, 'calving_thick'] = output_df.loc[ - 0, 'calving_thick' - ] - output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df.loc[ - 0, 'calving_flux_Gta' - ] - output_df_all.loc[nglac, 'no_errors'] = output_df.loc[ - 0, 'no_errors' - ] - output_df_all.loc[nglac, 'oggm_dynamics'] = output_df.loc[ - 0, 'oggm_dynamics' - ] + output_df_all.loc[nglac, 'calving_k'] = output_df.loc[0, 'calving_k'] + output_df_all.loc[nglac, 'calving_thick'] = output_df.loc[0, 'calving_thick'] + output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df.loc[0, 'calving_flux_Gta'] + output_df_all.loc[nglac, 'no_errors'] = output_df.loc[0, 'no_errors'] + output_df_all.loc[nglac, 'oggm_dynamics'] = output_df.loc[0, 'oggm_dynamics'] # ----- ADD UNCERTAINTY ----- # Upper uncertainty @@ -1270,8 +1135,7 @@ def calib_ind_calving_k( print('\n\n----- upper uncertainty:') fa_glac_data_ind_high = fa_glac_data_ind.copy() fa_gta_obs_high = ( - fa_glac_data_ind.loc[0, 'fa_gta_obs'] - + fa_glac_data_ind.loc[0, 'fa_gta_obs_unc'] + fa_glac_data_ind.loc[0, 'fa_gta_obs'] + fa_glac_data_ind.loc[0, 'fa_gta_obs_unc'] ) fa_glac_data_ind_high.loc[0, 'fa_gta_obs'] = fa_gta_obs_high calving_k_bndlow_upper = np.copy(calving_k_med) - 0.01 @@ -1303,13 +1167,10 @@ def calib_ind_calving_k( fa_glac_data_ind_low = fa_glac_data_ind.copy() fa_gta_obs_low = ( - fa_glac_data_ind.loc[0, 'fa_gta_obs'] - - fa_glac_data_ind.loc[0, 'fa_gta_obs_unc'] + fa_glac_data_ind.loc[0, 'fa_gta_obs'] - fa_glac_data_ind.loc[0, 'fa_gta_obs_unc'] ) if fa_gta_obs_low < 0: - calving_k_nmadlow = calving_k_med - abs( - calving_k_nmadhigh - calving_k_med - ) + calving_k_nmadlow = calving_k_med - abs(calving_k_nmadhigh - calving_k_med) if verbose: print( 'calving_k:', @@ -1337,9 +1198,7 @@ def calib_ind_calving_k( 'calving_k:', np.round(calving_k, 2), 'fa_data low:', - np.round( - fa_glac_data_ind_low.loc[0, 'fa_gta_obs'], 4 - ), + np.round(fa_glac_data_ind_low.loc[0, 'fa_gta_obs'], 4), 'fa_mod low:', np.round(output_df.loc[0, 'calving_flux_Gta'], 4), ) @@ -1388,9 +1247,7 @@ def calib_ind_calving_k( # ----- VIEW DIAGNOSTICS OF 'GOOD' GLACIERS ----- # special for 17 because so few 'good' glaciers if reg in [17]: - output_df_all_good = output_df_all.loc[ - (output_df_all['calving_k'] < calving_k_bndhigh_set), : - ] + output_df_all_good = output_df_all.loc[(output_df_all['calving_k'] < calving_k_bndhigh_set), :] else: output_df_all_good = output_df_all.loc[ (output_df_all['fa_gta_obs'] == output_df_all['fa_gta_max']) @@ -1408,9 +1265,7 @@ def calib_ind_calving_k( np.round(np.median(output_df_all_good.calving_k), 2), ) - output_df_all['calving_flux_Gta_rnd1'] = output_df_all[ - 'calving_flux_Gta' - ].copy() + output_df_all['calving_flux_Gta_rnd1'] = output_df_all['calving_flux_Gta'].copy() output_df_all['calving_k_rnd1'] = output_df_all['calving_k'].copy() # ----- PLOT RESULTS FOR EACH GLACIER ----- @@ -1436,9 +1291,7 @@ def calib_ind_calving_k( x_min, x_max = plot_min, plot_max - fig, ax = plt.subplots( - 2, 2, squeeze=False, gridspec_kw={'wspace': 0.4, 'hspace': 0.4} - ) + fig, ax = plt.subplots(2, 2, squeeze=False, gridspec_kw={'wspace': 0.4, 'hspace': 0.4}) # ----- Scatter plot ----- # Marker size @@ -1464,9 +1317,7 @@ def calib_ind_calving_k( ax[0, 0].set_ylabel('Modeled $A_{f}$ (Gt/yr)', size=12) ax[0, 0].set_xlim(x_min, x_max) ax[0, 0].set_ylim(x_min, x_max) - ax[0, 0].plot( - [x_min, x_max], [x_min, x_max], color='k', linewidth=0.5, zorder=1 - ) + ax[0, 0].plot([x_min, x_max], [x_min, x_max], color='k', linewidth=0.5, zorder=1) # Log scale ax[0, 0].set_xscale('log') ax[0, 0].set_yscale('log') @@ -1510,13 +1361,9 @@ def calib_ind_calving_k( # ----- Histogram ----- # nbins = 25 # ax[0,1].hist(output_df_all_good['calving_k'], bins=nbins, color='grey', edgecolor='k') - vn_bins = np.arange( - 0, np.max([1, output_df_all_good.calving_k.max()]) + 0.1, 0.1 - ) + vn_bins = np.arange(0, np.max([1, output_df_all_good.calving_k.max()]) + 0.1, 0.1) hist, bins = np.histogram( - output_df_all_good.loc[ - output_df_all_good['no_errors'] == 1, 'calving_k' - ], + output_df_all_good.loc[output_df_all_good['no_errors'] == 1, 'calving_k'], bins=vn_bins, ) ax[0, 1].bar( @@ -1533,15 +1380,11 @@ def calib_ind_calving_k( if hist.max() < 40: y_major_interval = 5 y_max = np.ceil(hist.max() / y_major_interval) * y_major_interval - ax[0, 1].set_yticks( - np.arange(0, y_max + y_major_interval, y_major_interval) - ) + ax[0, 1].set_yticks(np.arange(0, y_max + y_major_interval, y_major_interval)) elif hist.max() > 40: y_major_interval = 10 y_max = np.ceil(hist.max() / y_major_interval) * y_major_interval - ax[0, 1].set_yticks( - np.arange(0, y_max + y_major_interval, y_major_interval) - ) + ax[0, 1].set_yticks(np.arange(0, y_max + y_major_interval, y_major_interval)) # Labels ax[0, 1].set_xlabel('$k_{f}$ (yr$^{-1}$)', size=12) @@ -1601,25 +1444,17 @@ def calib_ind_calving_k( # Save figure fig.set_size_inches(6, 6) - fig_fullfn = ( - output_fp + str(reg) + '-frontalablation_glac_compare-cal_ind-good.png' - ) + fig_fullfn = output_fp + str(reg) + '-frontalablation_glac_compare-cal_ind-good.png' fig.savefig(fig_fullfn, bbox_inches='tight', dpi=300) # ----- REPLACE UPPER BOUND CALVING_K WITH MEDIAN CALVING_K ----- - rgiids_bndhigh = list( - output_df_all.loc[ - output_df_all['calving_k'] == calving_k_bndhigh_set, 'RGIId' - ].values - ) + rgiids_bndhigh = list(output_df_all.loc[output_df_all['calving_k'] == calving_k_bndhigh_set, 'RGIId'].values) for nglac, rgiid in enumerate(output_df_all.RGIId): if rgiid in rgiids_bndhigh: # Estimate frontal ablation for poor glaciers extrapolated from good ones main_glac_rgi_ind = main_glac_rgi.loc[main_glac_rgi.RGIId == rgiid, :] main_glac_rgi_ind.reset_index(inplace=True, drop=True) - fa_glac_data_ind = fa_glac_data_reg.loc[ - fa_glac_data_reg.RGIId == rgiid, : - ] + fa_glac_data_ind = fa_glac_data_reg.loc[fa_glac_data_reg.RGIId == rgiid, :] fa_glac_data_ind.reset_index(inplace=True, drop=True) calving_k = np.median(output_df_all_good.calving_k) @@ -1635,13 +1470,9 @@ def calib_ind_calving_k( ignore_nan=False, ) - output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df.loc[ - 0, 'calving_flux_Gta' - ] + output_df_all.loc[nglac, 'calving_flux_Gta'] = output_df.loc[0, 'calving_flux_Gta'] output_df_all.loc[nglac, 'calving_k'] = output_df.loc[0, 'calving_k'] - output_df_all.loc[nglac, 'calving_k_nmad'] = np.median( - output_df_all_good.calving_k_nmad - ) + output_df_all.loc[nglac, 'calving_k_nmad'] = np.median(output_df_all_good.calving_k_nmad) # ----- EXPORT MODEL RESULTS ----- output_df_all.to_csv(output_fp + output_fn, index=False) @@ -1664,9 +1495,7 @@ def calib_ind_calving_k( if len(rgiids_missing) == 0: break glac_no_missing = [x.split('-')[1] for x in rgiids_missing] - main_glac_rgi_missing = modelsetup.selectglaciersrgitable( - glac_no=glac_no_missing - ) + main_glac_rgi_missing = modelsetup.selectglaciersrgitable(glac_no=glac_no_missing) if verbose: print( @@ -1678,9 +1507,7 @@ def calib_ind_calving_k( if not os.path.exists(output_fp + output_fn_missing) or overwrite: # Add regions for median subsets - output_df_all['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in output_df_all.RGIId - ] + output_df_all['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in output_df_all.RGIId] # Update mass balance data output_df_missing = pd.DataFrame( @@ -1689,16 +1516,10 @@ def calib_ind_calving_k( ) output_df_missing['RGIId'] = rgiids_missing output_df_missing['fa_gta_obs'] = np.nan - rgi_area_dict = dict( - zip(main_glac_rgi_missing.RGIId, main_glac_rgi_missing.Area) - ) - output_df_missing['area_km2'] = output_df_missing['RGIId'].map( - rgi_area_dict - ) + rgi_area_dict = dict(zip(main_glac_rgi_missing.RGIId, main_glac_rgi_missing.Area)) + output_df_missing['area_km2'] = output_df_missing['RGIId'].map(rgi_area_dict) rgi_mbobs_dict = dict(zip(mb_data['rgiid'], mb_data['mb_mwea'])) - output_df_missing['mb_clim_mwea_obs'] = output_df_missing['RGIId'].map( - rgi_mbobs_dict - ) + output_df_missing['mb_clim_mwea_obs'] = output_df_missing['RGIId'].map(rgi_mbobs_dict) output_df_missing['mb_clim_gta_obs'] = [ mwea_to_gta( output_df_missing.loc[x, 'mb_clim_mwea_obs'], @@ -1706,67 +1527,48 @@ def calib_ind_calving_k( ) for x in output_df_missing.index ] - output_df_missing['mb_total_mwea_obs'] = output_df_missing[ - 'mb_clim_mwea_obs' - ] - output_df_missing['mb_total_gta_obs'] = output_df_missing[ - 'mb_total_gta_obs' - ] + output_df_missing['mb_total_mwea_obs'] = output_df_missing['mb_clim_mwea_obs'] + output_df_missing['mb_total_gta_obs'] = output_df_missing['mb_total_gta_obs'] # Start with median value - calving_k_med = np.median( - output_df_all.loc[output_df_all['O1Region'] == reg, 'calving_k'] - ) + calving_k_med = np.median(output_df_all.loc[output_df_all['O1Region'] == reg, 'calving_k']) failed_glacs = [] for nglac, rgiid in enumerate(rgiids_missing): glacier_str = rgiid.split('-')[1] try: - main_glac_rgi_ind = modelsetup.selectglaciersrgitable( - glac_no=[rgiid.split('-')[1]] - ) + main_glac_rgi_ind = modelsetup.selectglaciersrgitable(glac_no=[rgiid.split('-')[1]]) # Estimate frontal ablation for missing glaciers - output_df, reg_calving_gta_mod, reg_calving_gta_obs = ( - reg_calving_flux( - main_glac_rgi_ind, - calving_k_med, - args, - debug=True, - calc_mb_geo_correction=True, - ) + output_df, reg_calving_gta_mod, reg_calving_gta_obs = reg_calving_flux( + main_glac_rgi_ind, + calving_k_med, + args, + debug=True, + calc_mb_geo_correction=True, ) # Adjust climatic mass balance to account for the losses due to frontal ablation # add this loss because it'll come from frontal ablation instead of climatic mass balance mb_clim_fa_corrected = ( - output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] - + output_df.loc[0, 'mb_mwea_fa_asl_lost'] + output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] + output_df.loc[0, 'mb_mwea_fa_asl_lost'] ) mb_clim_reg_95 = mb_clim_reg_avg + 1.96 * mb_clim_reg_std if verbose: print( 'mb_clim (raw):', - np.round( - output_df_missing.loc[nglac, 'mb_clim_mwea_obs'], 2 - ), + np.round(output_df_missing.loc[nglac, 'mb_clim_mwea_obs'], 2), ) print( 'mb_clim (fa_corrected):', np.round(mb_clim_fa_corrected, 2), ) print('mb_clim (reg 95%):', np.round(mb_clim_reg_95, 2)) - print( - 'mb_total (95% min):', np.round(mb_clim_reg_3std_min, 2) - ) + print('mb_total (95% min):', np.round(mb_clim_reg_3std_min, 2)) # Set nmad to median value - correct if value reduced # calving_k_nmad_missing = 1.4826*median_abs_deviation(output_df_all_good.calving_k) - calving_k_nmad_missing = np.median( - output_df_all_good.calving_k_nmad - ) - output_df_missing.loc[nglac, 'calving_k_nmad'] = ( - calving_k_nmad_missing - ) + calving_k_nmad_missing = np.median(output_df_all_good.calving_k_nmad) + output_df_missing.loc[nglac, 'calving_k_nmad'] = calving_k_nmad_missing if mb_clim_fa_corrected < mb_clim_reg_95: for cn in [ @@ -1777,9 +1579,7 @@ def calib_ind_calving_k( 'oggm_dynamics', ]: output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_fa_corrected - ) + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_fa_corrected output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( output_df_missing.loc[nglac, 'mb_clim_mwea'], output_df_missing.loc[nglac, 'area_km2'] * 1e6, @@ -1796,10 +1596,7 @@ def calib_ind_calving_k( if mb_clim_fa_corrected > mb_clim_reg_95: # Calibrate frontal ablation based on fa_mwea_max # i.e., the maximum frontal ablation that is consistent with reasonable mb_clim - fa_mwea_max = ( - mb_clim_reg_95 - - output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] - ) + fa_mwea_max = mb_clim_reg_95 - output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] # Reset bounds calving_k = calving_k_med @@ -1811,9 +1608,7 @@ def calib_ind_calving_k( rgiid_ind = main_glac_rgi_ind.loc[0, 'RGIId'] # fa_glac_data_ind = pd.DataFrame(np.zeros((1,len(fa_glac_data_reg.columns))), # columns=fa_glac_data_reg.columns) - fa_glac_data_ind = pd.DataFrame( - columns=fa_glac_data_reg.columns - ) + fa_glac_data_ind = pd.DataFrame(columns=fa_glac_data_reg.columns) fa_glac_data_ind.loc[0, 'RGIId'] = rgiid_ind # Check bounds @@ -1864,12 +1659,8 @@ def calib_ind_calving_k( ) # Record bounds - output_df_missing.loc[nglac, 'calving_flux_Gta_bndlow'] = ( - reg_calving_gta_mod_bndlow - ) - output_df_missing.loc[nglac, 'calving_flux_Gta_bndhigh'] = ( - reg_calving_gta_mod_bndhigh - ) + output_df_missing.loc[nglac, 'calving_flux_Gta_bndlow'] = reg_calving_gta_mod_bndlow + output_df_missing.loc[nglac, 'calving_flux_Gta_bndhigh'] = reg_calving_gta_mod_bndhigh if verbose: print( @@ -1884,18 +1675,11 @@ def calib_ind_calving_k( run_opt = True if fa_mwea_max > 0: if bndhigh_good and bndlow_good: - if ( - fa_mwea_max - < output_df_bndlow.loc[0, 'mb_mwea_fa_asl_lost'] - ): + if fa_mwea_max < output_df_bndlow.loc[0, 'mb_mwea_fa_asl_lost']: # Adjust climatic mass balance to note account for the losses due to frontal ablation mb_clim_fa_corrected = ( - output_df_missing.loc[ - nglac, 'mb_clim_mwea_obs' - ] - + output_df_bndlow.loc[ - 0, 'mb_mwea_fa_asl_lost' - ] + output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] + + output_df_bndlow.loc[0, 'mb_mwea_fa_asl_lost'] ) # Record output for cn in [ @@ -1905,50 +1689,30 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = ( - output_df_bndlow.loc[0, cn] - ) - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_fa_corrected - ) - output_df_missing.loc[nglac, 'mb_clim_gta'] = ( - mwea_to_gta( - output_df_missing.loc[ - nglac, 'mb_clim_mwea' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, - ) + output_df_missing.loc[nglac, cn] = output_df_bndlow.loc[0, cn] + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_fa_corrected + output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_clim_mwea'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) output_df_missing.loc[nglac, 'mb_total_gta'] = ( output_df_missing.loc[nglac, 'mb_clim_gta'] - - output_df_missing.loc[ - nglac, 'calving_flux_Gta' - ] + - output_df_missing.loc[nglac, 'calving_flux_Gta'] ) - output_df_missing.loc[ - nglac, 'mb_total_mwea' - ] = gta_to_mwea( - output_df_missing.loc[ - nglac, 'mb_total_gta' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, + output_df_missing.loc[nglac, 'mb_total_mwea'] = gta_to_mwea( + output_df_missing.loc[nglac, 'mb_total_gta'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) run_opt = False elif ( output_df_bndhigh.loc[0, 'mb_mwea_fa_asl_lost'] - == output_df_bndlow.loc[ - 0, 'mb_mwea_fa_asl_lost' - ] + == output_df_bndlow.loc[0, 'mb_mwea_fa_asl_lost'] ): # Adjust climatic mass balance to note account for the losses due to frontal ablation mb_clim_fa_corrected = ( - output_df_missing.loc[ - nglac, 'mb_clim_mwea_obs' - ] + output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] + output_df.loc[0, 'mb_mwea_fa_asl_lost'] ) # Record output @@ -1959,35 +1723,19 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = ( - output_df.loc[0, cn] - ) - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_fa_corrected - ) - output_df_missing.loc[nglac, 'mb_clim_gta'] = ( - mwea_to_gta( - output_df_missing.loc[ - nglac, 'mb_clim_mwea' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, - ) + output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_fa_corrected + output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_clim_mwea'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) output_df_missing.loc[nglac, 'mb_total_gta'] = ( output_df_missing.loc[nglac, 'mb_clim_gta'] - - output_df_missing.loc[ - nglac, 'calving_flux_Gta' - ] + - output_df_missing.loc[nglac, 'calving_flux_Gta'] ) - output_df_missing.loc[ - nglac, 'mb_total_mwea' - ] = gta_to_mwea( - output_df_missing.loc[ - nglac, 'mb_total_gta' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, + output_df_missing.loc[nglac, 'mb_total_mwea'] = gta_to_mwea( + output_df_missing.loc[nglac, 'mb_total_gta'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) run_opt = False @@ -1999,9 +1747,7 @@ def calib_ind_calving_k( print( 'mb_clim_obs:', np.round( - output_df_missing.loc[ - nglac, 'mb_clim_mwea_obs' - ], + output_df_missing.loc[nglac, 'mb_clim_mwea_obs'], 2, ), ) @@ -2010,15 +1756,9 @@ def calib_ind_calving_k( np.round(mb_clim_fa_corrected, 2), ) - calving_k_step_missing = ( - calving_k_med - calving_k_bndlow - ) / 20 + calving_k_step_missing = (calving_k_med - calving_k_bndlow) / 20 calving_k_next = calving_k - calving_k_step_missing - while ( - output_df.loc[0, 'mb_mwea_fa_asl_lost'] - > fa_mwea_max - and calving_k_next > 0 - ): + while output_df.loc[0, 'mb_mwea_fa_asl_lost'] > fa_mwea_max and calving_k_next > 0: calving_k -= calving_k_step_missing # Estimate frontal ablation for missing glaciers @@ -2034,9 +1774,7 @@ def calib_ind_calving_k( calc_mb_geo_correction=True, ) - calving_k_next = ( - calving_k - calving_k_step_missing - ) + calving_k_next = calving_k - calving_k_step_missing # Adjust climatic mass balance to note account for the losses due to frontal ablation mb_clim_fa_corrected = ( @@ -2051,35 +1789,19 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = ( - output_df.loc[0, cn] - ) - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_fa_corrected - ) - output_df_missing.loc[nglac, 'mb_clim_gta'] = ( - mwea_to_gta( - output_df_missing.loc[ - nglac, 'mb_clim_mwea' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, - ) + output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_fa_corrected + output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_clim_mwea'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) output_df_missing.loc[nglac, 'mb_total_gta'] = ( output_df_missing.loc[nglac, 'mb_clim_gta'] - - output_df_missing.loc[ - nglac, 'calving_flux_Gta' - ] + - output_df_missing.loc[nglac, 'calving_flux_Gta'] ) - output_df_missing.loc[nglac, 'mb_total_mwea'] = ( - gta_to_mwea( - output_df_missing.loc[ - nglac, 'mb_total_gta' - ], - output_df_missing.loc[nglac, 'area_km2'] - * 1e6, - ) + output_df_missing.loc[nglac, 'mb_total_mwea'] = gta_to_mwea( + output_df_missing.loc[nglac, 'mb_total_gta'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) if verbose: @@ -2091,14 +1813,11 @@ def calib_ind_calving_k( # If mass balance is higher than 95% threshold, then just make sure correction is reasonable (no more than 10%) else: calving_k = calving_k_med - calving_k_step_missing = ( - calving_k_med - calving_k_bndlow - ) / 20 + calving_k_step_missing = (calving_k_med - calving_k_bndlow) / 20 calving_k_next = calving_k - calving_k_step_missing while ( output_df.loc[0, 'mb_mwea_fa_asl_lost'] - > 0.1 - * output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] + > 0.1 * output_df_missing.loc[nglac, 'mb_clim_mwea_obs'] and calving_k_next > 0 ): calving_k -= calving_k_step_missing @@ -2131,27 +1850,19 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = output_df.loc[ - 0, cn - ] - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_fa_corrected - ) - output_df_missing.loc[nglac, 'mb_clim_gta'] = ( - mwea_to_gta( - output_df_missing.loc[nglac, 'mb_clim_mwea'], - output_df_missing.loc[nglac, 'area_km2'] * 1e6, - ) + output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_fa_corrected + output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_clim_mwea'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) output_df_missing.loc[nglac, 'mb_total_gta'] = ( output_df_missing.loc[nglac, 'mb_clim_gta'] - output_df_missing.loc[nglac, 'calving_flux_Gta'] ) - output_df_missing.loc[nglac, 'mb_total_mwea'] = ( - gta_to_mwea( - output_df_missing.loc[nglac, 'mb_total_gta'], - output_df_missing.loc[nglac, 'area_km2'] * 1e6, - ) + output_df_missing.loc[nglac, 'mb_total_mwea'] = gta_to_mwea( + output_df_missing.loc[nglac, 'mb_total_gta'], + output_df_missing.loc[nglac, 'area_km2'] * 1e6, ) if verbose: @@ -2161,13 +1872,9 @@ def calib_ind_calving_k( ) # Adjust calving_k_nmad if calving_k is very low to avoid poor values - if ( - output_df_missing.loc[nglac, 'calving_k'] - < calving_k_nmad_missing - ): + if output_df_missing.loc[nglac, 'calving_k'] < calving_k_nmad_missing: output_df_missing.loc[nglac, 'calving_k_nmad'] = ( - output_df_missing.loc[nglac, 'calving_k'] - - calving_k_bndlow_set + output_df_missing.loc[nglac, 'calving_k'] - calving_k_bndlow_set ) except: failed_glacs.append(glacier_str) @@ -2177,9 +1884,7 @@ def calib_ind_calving_k( # Write list of failed glaciers if len(failed_glacs) > 0: - with open( - output_fp + output_fn_missing[:-4] + '-failed.txt', 'w' - ) as f: + with open(output_fp + output_fn_missing[:-4] + '-failed.txt', 'w') as f: for item in failed_glacs: f.write(f'{item}\n') else: @@ -2218,9 +1923,7 @@ def calib_ind_calving_k( rgiids_missing = [x for x in rgiids_all if x not in rgiids_processed] glac_no_missing = [x.split('-')[1] for x in rgiids_missing] - main_glac_rgi_missing = modelsetup.selectglaciersrgitable( - glac_no=glac_no_missing - ) + main_glac_rgi_missing = modelsetup.selectglaciersrgitable(glac_no=glac_no_missing) if verbose: print( @@ -2232,9 +1935,7 @@ def calib_ind_calving_k( if not os.path.exists(output_fp + output_fn_missing) or overwrite: # Add regions for median subsets - output_df_all['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in output_df_all.RGIId - ] + output_df_all['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in output_df_all.RGIId] # Update mass balance data output_df_missing = pd.DataFrame( @@ -2243,16 +1944,10 @@ def calib_ind_calving_k( ) output_df_missing['RGIId'] = rgiids_missing output_df_missing['fa_gta_obs'] = np.nan - rgi_area_dict = dict( - zip(main_glac_rgi_missing.RGIId, main_glac_rgi_missing.Area) - ) - output_df_missing['area_km2'] = output_df_missing['RGIId'].map( - rgi_area_dict - ) + rgi_area_dict = dict(zip(main_glac_rgi_missing.RGIId, main_glac_rgi_missing.Area)) + output_df_missing['area_km2'] = output_df_missing['RGIId'].map(rgi_area_dict) rgi_mbobs_dict = dict(zip(mb_data['rgiid'], mb_data['mb_mwea'])) - output_df_missing['mb_clim_mwea_obs'] = output_df_missing['RGIId'].map( - rgi_mbobs_dict - ) + output_df_missing['mb_clim_mwea_obs'] = output_df_missing['RGIId'].map(rgi_mbobs_dict) output_df_missing['mb_clim_gta_obs'] = [ mwea_to_gta( output_df_missing.loc[x, 'mb_clim_mwea_obs'], @@ -2260,12 +1955,8 @@ def calib_ind_calving_k( ) for x in output_df_missing.index ] - output_df_missing['mb_total_mwea_obs'] = output_df_missing[ - 'mb_clim_mwea_obs' - ] - output_df_missing['mb_total_gta_obs'] = output_df_missing[ - 'mb_clim_gta_obs' - ] + output_df_missing['mb_total_mwea_obs'] = output_df_missing['mb_clim_mwea_obs'] + output_df_missing['mb_total_gta_obs'] = output_df_missing['mb_clim_gta_obs'] # Uncertainty with calving_k based on regional calibration # calving_k_nmad_missing = 1.4826 * median_abs_deviation(output_df_all.calving_k) @@ -2276,33 +1967,23 @@ def calib_ind_calving_k( mb_clim_reg_95 = mb_clim_reg_avg + 1.96 * mb_clim_reg_std # Start with median value - calving_k_med = np.median( - output_df_all.loc[output_df_all['O1Region'] == reg, 'calving_k'] - ) + calving_k_med = np.median(output_df_all.loc[output_df_all['O1Region'] == reg, 'calving_k']) for nglac, rgiid in enumerate(rgiids_missing): try: - main_glac_rgi_ind = modelsetup.selectglaciersrgitable( - glac_no=[rgiid.split('-')[1]] - ) + main_glac_rgi_ind = modelsetup.selectglaciersrgitable(glac_no=[rgiid.split('-')[1]]) area_km2 = main_glac_rgi_ind.loc[0, 'Area'] # Estimate frontal ablation for missing glaciers - output_df, reg_calving_gta_mod, reg_calving_gta_obs = ( - reg_calving_flux( - main_glac_rgi_ind, - calving_k_med, - args, - debug=True, - calc_mb_geo_correction=True, - ) + output_df, reg_calving_gta_mod, reg_calving_gta_obs = reg_calving_flux( + main_glac_rgi_ind, + calving_k_med, + args, + debug=True, + calc_mb_geo_correction=True, ) # ASSUME THE TOTAL MASS BALANCE EQUALS THE GEODETIC MASS BALANCE CORRECTED FOR THE FA BELOW SEA LEVEL - mb_total_mwea = output_df_missing.loc[ - nglac, 'mb_total_mwea_obs' - ] - mb_fa_mwea = gta_to_mwea( - output_df.loc[0, 'calving_flux_Gta'], area_km2 * 1e6 - ) + mb_total_mwea = output_df_missing.loc[nglac, 'mb_total_mwea_obs'] + mb_fa_mwea = gta_to_mwea(output_df.loc[0, 'calving_flux_Gta'], area_km2 * 1e6) mb_clim_mwea = mb_total_mwea + mb_fa_mwea if verbose: @@ -2326,9 +2007,7 @@ def calib_ind_calving_k( output_df_missing.loc[nglac, 'mb_clim_mwea'], area_km2 * 1e6, ) - output_df_missing.loc[nglac, 'mb_total_mwea'] = ( - mb_total_mwea - ) + output_df_missing.loc[nglac, 'mb_total_mwea'] = mb_total_mwea output_df_missing.loc[nglac, 'mb_total_gta'] = mwea_to_gta( output_df_missing.loc[nglac, 'mb_total_gta'], area_km2 * 1e6, @@ -2342,9 +2021,7 @@ def calib_ind_calving_k( # therefore, correct it to only let it be 10% of the positive mb_total such that it stays "reasonable" if fa_mwea_max < 0: if verbose: - print( - '\n too positive, limiting fa_mwea_max to 10% mb_total_mwea' - ) + print('\n too positive, limiting fa_mwea_max to 10% mb_total_mwea') fa_mwea_max = 0.1 * mb_total_mwea # Reset bounds @@ -2357,9 +2034,7 @@ def calib_ind_calving_k( rgiid_ind = main_glac_rgi_ind.loc[0, 'RGIId'] # fa_glac_data_ind = pd.DataFrame(np.zeros((1,len(fa_glac_data_reg.columns))), # columns=fa_glac_data_reg.columns) - fa_glac_data_ind = pd.DataFrame( - columns=fa_glac_data_reg.columns - ) + fa_glac_data_ind = pd.DataFrame(columns=fa_glac_data_reg.columns) fa_glac_data_ind.loc[0, 'RGIId'] = rgiid_ind # Check bounds @@ -2400,29 +2075,21 @@ def calib_ind_calving_k( reg_calving_gta_mod_bndlow = None # Record bounds - output_df_missing.loc[nglac, 'calving_flux_Gta_bndlow'] = ( - reg_calving_gta_mod_bndlow - ) - output_df_missing.loc[nglac, 'calving_flux_Gta_bndhigh'] = ( - reg_calving_gta_mod_bndhigh - ) + output_df_missing.loc[nglac, 'calving_flux_Gta_bndlow'] = reg_calving_gta_mod_bndlow + output_df_missing.loc[nglac, 'calving_flux_Gta_bndhigh'] = reg_calving_gta_mod_bndhigh if verbose: print( ' fa_model_bndlow [mwea] :', np.round( - gta_to_mwea( - reg_calving_gta_mod_bndlow, area_km2 * 1e6 - ), + gta_to_mwea(reg_calving_gta_mod_bndlow, area_km2 * 1e6), 2, ), ) print( ' fa_model_bndhigh [mwea] :', np.round( - gta_to_mwea( - reg_calving_gta_mod_bndhigh, area_km2 * 1e6 - ), + gta_to_mwea(reg_calving_gta_mod_bndhigh, area_km2 * 1e6), 2, ), ) @@ -2433,9 +2100,7 @@ def calib_ind_calving_k( print('\n-------') print('mb_clim_mwea:', np.round(mb_clim_mwea, 2)) - calving_k_step_missing = ( - calving_k_med - calving_k_bndlow - ) / 20 + calving_k_step_missing = (calving_k_med - calving_k_bndlow) / 20 calving_k_next = calving_k - calving_k_step_missing ncount = 0 while mb_fa_mwea > fa_mwea_max and calving_k_next > 0: @@ -2483,9 +2148,7 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = output_df.loc[ - 0, cn - ] + output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] mb_clim_mwea = mb_total_mwea + mb_fa_mwea if verbose: @@ -2504,36 +2167,22 @@ def calib_ind_calving_k( 'no_errors', 'oggm_dynamics', ]: - output_df_missing.loc[nglac, cn] = output_df.loc[ - 0, cn - ] - output_df_missing.loc[nglac, 'mb_clim_mwea'] = ( - mb_clim_mwea + output_df_missing.loc[nglac, cn] = output_df.loc[0, cn] + output_df_missing.loc[nglac, 'mb_clim_mwea'] = mb_clim_mwea + output_df_missing.loc[nglac, 'mb_clim_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_clim_mwea'], + area_km2 * 1e6, ) - output_df_missing.loc[nglac, 'mb_clim_gta'] = ( - mwea_to_gta( - output_df_missing.loc[nglac, 'mb_clim_mwea'], - area_km2 * 1e6, - ) - ) - output_df_missing.loc[nglac, 'mb_total_mwea'] = ( - mb_total_mwea - ) - output_df_missing.loc[nglac, 'mb_total_gta'] = ( - mwea_to_gta( - output_df_missing.loc[nglac, 'mb_total_mwea'], - area_km2 * 1e6, - ) + output_df_missing.loc[nglac, 'mb_total_mwea'] = mb_total_mwea + output_df_missing.loc[nglac, 'mb_total_gta'] = mwea_to_gta( + output_df_missing.loc[nglac, 'mb_total_mwea'], + area_km2 * 1e6, ) # Adjust calving_k_nmad if calving_k is very low to avoid poor values - if ( - output_df_missing.loc[nglac, 'calving_k'] - < calving_k_nmad_missing - ): + if output_df_missing.loc[nglac, 'calving_k'] < calving_k_nmad_missing: output_df_missing.loc[nglac, 'calving_k_nmad'] = ( - output_df_missing.loc[nglac, 'calving_k'] - - calving_k_bndlow_set + output_df_missing.loc[nglac, 'calving_k'] - calving_k_bndlow_set ) # # Check uncertainty based on NMAD @@ -2572,23 +2221,17 @@ def calib_ind_calving_k( # ----- PLOT RESULTS FOR EACH GLACIER ----- with np.errstate(all='ignore'): - plot_max_raw = np.max( - [output_df_all.calving_flux_Gta.max(), output_df_all.fa_gta_obs.max()] - ) + plot_max_raw = np.max([output_df_all.calving_flux_Gta.max(), output_df_all.fa_gta_obs.max()]) plot_max = 10 ** np.ceil(np.log10(plot_max_raw)) - plot_min_raw = np.max( - [output_df_all.calving_flux_Gta.min(), output_df_all.fa_gta_obs.min()] - ) + plot_min_raw = np.max([output_df_all.calving_flux_Gta.min(), output_df_all.fa_gta_obs.min()]) plot_min = 10 ** np.floor(np.log10(plot_min_raw)) if plot_min < 1e-3: plot_min = 1e-4 x_min, x_max = plot_min, plot_max - fig, ax = plt.subplots( - 2, 2, squeeze=False, gridspec_kw={'wspace': 0.3, 'hspace': 0.3} - ) + fig, ax = plt.subplots(2, 2, squeeze=False, gridspec_kw={'wspace': 0.3, 'hspace': 0.3}) # ----- Scatter plot ----- # Marker size @@ -2614,9 +2257,7 @@ def calib_ind_calving_k( ax[0, 0].set_ylabel('Modeled $A_{f}$ (Gt/yr)', size=12) ax[0, 0].set_xlim(x_min, x_max) ax[0, 0].set_ylim(x_min, x_max) - ax[0, 0].plot( - [x_min, x_max], [x_min, x_max], color='k', linewidth=0.5, zorder=1 - ) + ax[0, 0].plot([x_min, x_max], [x_min, x_max], color='k', linewidth=0.5, zorder=1) # Log scale ax[0, 0].set_xscale('log') ax[0, 0].set_yscale('log') @@ -2683,15 +2324,11 @@ def calib_ind_calving_k( if hist.max() < 40: y_major_interval = 5 y_max = np.ceil(hist.max() / y_major_interval) * y_major_interval - ax[0, 1].set_yticks( - np.arange(0, y_max + y_major_interval, y_major_interval) - ) + ax[0, 1].set_yticks(np.arange(0, y_max + y_major_interval, y_major_interval)) elif hist.max() > 40: y_major_interval = 10 y_max = np.ceil(hist.max() / y_major_interval) * y_major_interval - ax[0, 1].set_yticks( - np.arange(0, y_max + y_major_interval, y_major_interval) - ) + ax[0, 1].set_yticks(np.arange(0, y_max + y_major_interval, y_major_interval)) # Labels ax[0, 1].set_xlabel('$k_{f}$ (yr$^{-1}$)', size=12) @@ -2733,9 +2370,7 @@ def calib_ind_calving_k( # ----- MERGE CALIBRATED CALVING DATASETS ----- -def merge_ind_calving_k( - regions=list(range(1, 20)), output_fp='', merged_calving_k_fn='', verbose=False -): +def merge_ind_calving_k(regions=list(range(1, 20)), output_fp='', merged_calving_k_fn='', verbose=False): # get list of all regional frontal ablation calibration file names output_reg_fns = sorted(glob.glob(f'{output_fp}/*-frontalablation_cal_ind.csv')) output_reg_fns = [x.split('/')[-1] for x in output_reg_fns] @@ -2755,13 +2390,9 @@ def merge_ind_calving_k( output_fn_reg_missing = output_fn_reg.replace('.csv', '-missing.csv') if os.path.exists(output_fp + output_fn_reg_missing): # Check if second correction exists - output_fn_reg_missing_v2 = output_fn_reg_missing.replace( - '.csv', '_wmbtotal_correction.csv' - ) + output_fn_reg_missing_v2 = output_fn_reg_missing.replace('.csv', '_wmbtotal_correction.csv') if os.path.exists(output_fp + output_fn_reg_missing_v2): - output_df_reg_missing = pd.read_csv( - output_fp + output_fn_reg_missing_v2 - ) + output_df_reg_missing = pd.read_csv(output_fp + output_fn_reg_missing_v2) else: output_df_reg_missing = pd.read_csv(output_fp + output_fn_reg_missing) @@ -2816,9 +2447,7 @@ def update_mbdata( # Update the mass balance data in Romain's file mb_idx = mb_rgiids.index(rgiid) mb_data.loc[mb_idx, 'mb_mwea'] = fa_glac_data.loc[nglac, 'mb_total_mwea'] - mb_data.loc[mb_idx, 'mb_clim_mwea'] = fa_glac_data.loc[ - nglac, 'mb_clim_mwea' - ] + mb_data.loc[mb_idx, 'mb_clim_mwea'] = fa_glac_data.loc[nglac, 'mb_clim_mwea'] if verbose: print( @@ -2843,9 +2472,7 @@ def update_mbdata( glacier_str = rgiid.split('-')[1] glac_strs.append(glacier_str) # paralllelize - func_ = partial( - single_flowline_glacier_directory_with_calving, reset=True, facorrected=True - ) + func_ = partial(single_flowline_glacier_directory_with_calving, reset=True, facorrected=True) with multiprocessing.Pool(ncores) as p: p.map(func_, glac_strs) @@ -3116,10 +2743,10 @@ def main(): args.ncores = int(np.min([njobs, args.ncores])) # data paths - frontalablation_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}' - frontalablation_cal_fn = pygem_prms['calib']['data']['frontalablation'][ - 'frontalablation_cal_fn' - ] + frontalablation_fp = ( + f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}' + ) + frontalablation_cal_fn = pygem_prms['calib']['data']['frontalablation']['frontalablation_cal_fn'] output_fp = frontalablation_fp + '/analysis/' hugonnet2021_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}/{args.hugonnet2021_fn}' hugonnet2021_facorr_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}/{args.hugonnet2021_facorrected_fn}' diff --git a/pygem/bin/run/run_calibration_reg_glena.py b/pygem/bin/run/run_calibration_reg_glena.py index 29cfd19e..d5503265 100644 --- a/pygem/bin/run/run_calibration_reg_glena.py +++ b/pygem/bin/run/run_calibration_reg_glena.py @@ -177,9 +177,7 @@ def plot_nfls_section(nfls): ] axh = fig.add_axes(posax, frameon=False) - axh.hist( - height, orientation='horizontal', range=ylim, bins=20, alpha=0.3, weights=area - ) + axh.hist(height, orientation='horizontal', range=ylim, bins=20, alpha=0.3, weights=area) axh.invert_xaxis() axh.xaxis.tick_top() axh.set_xlabel('Area incl. tributaries (km$^2$)') @@ -251,9 +249,7 @@ def reg_vol_comparison(gdirs, mbmods, a_multiplier=1, fs=0, debug=False): apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) tasks.prepare_for_inversion(gdir) - tasks.mass_conservation_inversion( - gdir, glen_a=cfg.PARAMS['glen_a'] * a_multiplier, fs=fs - ) + tasks.mass_conservation_inversion(gdir, glen_a=cfg.PARAMS['glen_a'] * a_multiplier, fs=fs) tasks.init_present_time_glacier(gdir) # adds bins below nfls = gdir.read_pickle('model_flowlines') @@ -261,9 +257,7 @@ def reg_vol_comparison(gdirs, mbmods, a_multiplier=1, fs=0, debug=False): if os.path.exists(gdir.get_filepath('consensus_mass')): consensus_fn = gdir.get_filepath('consensus_mass') with open(consensus_fn, 'rb') as f: - consensus_km3 = ( - pickle.load(f) / pygem_prms['constants']['density_ice'] / 1e9 - ) + consensus_km3 = pickle.load(f) / pygem_prms['constants']['density_ice'] / 1e9 reg_vol_km3_consensus += consensus_km3 reg_vol_km3_modeled += nfls[0].volume_km3 @@ -304,18 +298,11 @@ def main(): main_glac_rgi_all = main_glac_rgi_all.sort_values('Area', ascending=False) main_glac_rgi_all.reset_index(inplace=True, drop=True) main_glac_rgi_all['Area_cum'] = np.cumsum(main_glac_rgi_all['Area']) - main_glac_rgi_all['Area_cum_frac'] = ( - main_glac_rgi_all['Area_cum'] / main_glac_rgi_all.Area.sum() - ) + main_glac_rgi_all['Area_cum_frac'] = main_glac_rgi_all['Area_cum'] / main_glac_rgi_all.Area.sum() - glac_idx = np.where( - main_glac_rgi_all.Area_cum_frac - > pygem_prms['calib']['icethickness_cal_frac_byarea'] - )[0][0] + glac_idx = np.where(main_glac_rgi_all.Area_cum_frac > pygem_prms['calib']['icethickness_cal_frac_byarea'])[0][0] main_glac_rgi_subset = main_glac_rgi_all.loc[0:glac_idx, :] - main_glac_rgi_subset = main_glac_rgi_subset.sort_values( - 'O1Index', ascending=True - ) + main_glac_rgi_subset = main_glac_rgi_subset.sort_values('O1Index', ascending=True) main_glac_rgi_subset.reset_index(inplace=True, drop=True) print( @@ -335,9 +322,7 @@ def main(): # ===== LOAD CLIMATE DATA ===== # Climate class sim_climate_name = args.ref_climate_name - assert sim_climate_name in ['ERA5', 'ERA-Interim'], ( - 'Error: Calibration not set up for ' + sim_climate_name - ) + assert sim_climate_name == 'ERA5', 'Error: Calibration not set up for ' + sim_climate_name gcm = class_climate.GCM(name=sim_climate_name) # Air temperature [degC] gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( @@ -358,9 +343,7 @@ def main(): gcm.prec_fn, gcm.prec_vn, main_glac_rgi_subset, dates_table, verbose=debug ) # Elevation [m asl] - gcm_elev = gcm.importGCMfxnearestneighbor_xarray( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi_subset - ) + gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi_subset) # Lapse rate [degC m-1] gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( gcm.lr_fn, gcm.lr_vn, main_glac_rgi_subset, dates_table, verbose=debug @@ -379,9 +362,7 @@ def main(): gdirs = [] for glac in range(main_glac_rgi_subset.shape[0]): # Select subsets of data - glacier_rgi_table = main_glac_rgi_subset.loc[ - main_glac_rgi_subset.index.values[glac], : - ] + glacier_rgi_table = main_glac_rgi_subset.loc[main_glac_rgi_subset.index.values[glac], :] glacier_str = '{0:0.5f}'.format(glacier_rgi_table['RGIId_float']) if glac % 1000 == 0: @@ -408,21 +389,14 @@ def main(): if (fls is not None) and (glacier_area_km2.sum() > 0): modelprms_fn = glacier_str + '-modelprms_dict.json' modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' + pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' ) modelprms_fullfn = modelprms_fp + modelprms_fn - assert os.path.exists(modelprms_fullfn), ( - glacier_str + ' calibrated parameters do not exist.' - ) + assert os.path.exists(modelprms_fullfn), glacier_str + ' calibrated parameters do not exist.' with open(modelprms_fullfn, 'r') as f: modelprms_dict = json.load(f) - assert 'emulator' in modelprms_dict, ( - 'Error: ' + glacier_str + ' emulator not in modelprms_dict' - ) + assert 'emulator' in modelprms_dict, 'Error: ' + glacier_str + ' emulator not in modelprms_dict' modelprms_all = modelprms_dict['emulator'] # Loop through model parameters @@ -536,9 +510,7 @@ def to_minimize(a_multiplier): debug=debug, ) - print( - '\n\nOptimized:\n glens_a_multiplier:', np.round(a_multiplier_opt, 3) - ) + print('\n\nOptimized:\n glens_a_multiplier:', np.round(a_multiplier_opt, 3)) print(' Consensus [km3]:', reg_vol_km3_con) print(' Model [km3] :', reg_vol_km3_mod) @@ -562,9 +534,7 @@ def to_minimize(a_multiplier): ] try: - glena_df = pd.read_csv( - f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}' - ) + glena_df = pd.read_csv(f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}') # Add or overwrite existing file glena_idx = np.where((glena_df.O1Region == reg))[0] diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index aff4c5e6..7d60e3b5 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -25,19 +25,15 @@ from pygem.utils._funcs import str2bool cfg.initialize() -cfg.PATHS['working_dir'] = ( - f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' -) +cfg.PATHS['working_dir'] = f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' -def run( - glac_no, ncores=1, calibrate_regional_glen_a=False, reset_gdirs=False, debug=False -): +def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None, reset_gdirs=False, debug=False): """ Run OGGM's bed inversion for a list of RGI glacier IDs using PyGEM's mass balance model. """ - update_cfg({'continue_on_error': False}, 'PARAMS') + update_cfg({'continue_on_error': True}, 'PARAMS') if ncores > 1: update_cfg({'use_multiprocessing': True}, 'PARAMS') update_cfg({'mp_processes': ncores}, 'PARAMS') @@ -77,20 +73,14 @@ def run( ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt, verbose=debug ) # Elevation [m asl] - elev = ref_clim.importGCMfxnearestneighbor_xarray( - ref_clim.elev_fn, ref_clim.elev_vn, main_glac_rgi - ) + elev = ref_clim.importGCMfxnearestneighbor_xarray(ref_clim.elev_fn, ref_clim.elev_vn, main_glac_rgi) # Lapse rate [degC m-1] lr, _ = ref_clim.importGCMvarnearestneighbor_xarray( ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt, verbose=debug ) # load prior regionally averaged modelprms (from Rounce et al. 2023) - priors_df = pd.read_csv( - pygem_prms['root'] - + '/Output/calibration/' - + pygem_prms['calib']['priors_reg_fn'] - ) + priors_df = pd.read_csv(pygem_prms['root'] + '/Output/calibration/' + pygem_prms['calib']['priors_reg_fn']) # loop through gdirs and add `glacier_rgi_table`, `historical_climate`, `dates_table` and `modelprms` attributes to each glacier directory for i, gd in enumerate(gdirs): @@ -157,9 +147,7 @@ def run( ), ) # add debris data to flowlines - workflow.execute_entity_task( - debris.debris_binned, gdirs, fl_str='inversion_flowlines' - ) + workflow.execute_entity_task(debris.debris_binned, gdirs, fl_str='inversion_flowlines') ########################## ### CALIBRATE GLEN'S A ### @@ -183,33 +171,26 @@ def run( glen_a = gdir.get_diagnostics()['inversion_glen_a'] fs = gdir.get_diagnostics()['inversion_fs'] else: - # get glen_a and fs values from prior calibration or manual entry - if pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a']: - glen_a_df = pd.read_csv( - f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}' - ) - glen_a_O1regions = [int(x) for x in glen_a_df.O1Region.values] - assert gdir.glacier_rgi_table.O1Region in glen_a_O1regions, ( - '{0:0.5f}'.format(gd.glacier_rgi_table['RGIId_float']) - + ' O1 region not in glen_a_df' - ) - glen_a_idx = np.where( - glen_a_O1regions == gdir.glacier_rgi_table.O1Region - )[0][0] - glen_a_multiplier = glen_a_df.loc[glen_a_idx, 'glens_a_multiplier'] - fs = glen_a_df.loc[glen_a_idx, 'fs'] - else: - glen_a_multiplier = pygem_prms['sim']['oggm_dynamics'][ - 'glen_a_multiplier' - ] - fs = pygem_prms['sim']['oggm_dynamics']['fs'] - glen_a = cfg.PARAMS['glen_a'] * glen_a_multiplier + if glen_a is None and fs is None: + # get glen_a and fs values from prior calibration or manual entry + if pygem_prms['sim']['oggm_dynamics']['use_regional_glen_a']: + glen_a_df = pd.read_csv( + f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}' + ) + glen_a_O1regions = [int(x) for x in glen_a_df.O1Region.values] + assert gdir.glacier_rgi_table.O1Region in glen_a_O1regions, ( + '{0:0.5f}'.format(gd.glacier_rgi_table['RGIId_float']) + ' O1 region not in glen_a_df' + ) + glen_a_idx = np.where(glen_a_O1regions == gdir.glacier_rgi_table.O1Region)[0][0] + glen_a_multiplier = glen_a_df.loc[glen_a_idx, 'glens_a_multiplier'] + fs = glen_a_df.loc[glen_a_idx, 'fs'] + else: + glen_a_multiplier = pygem_prms['sim']['oggm_dynamics']['glen_a_multiplier'] + fs = pygem_prms['sim']['oggm_dynamics']['fs'] + glen_a = cfg.PARAMS['glen_a'] * glen_a_multiplier # non-tidewater - if ( - gdir.glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_frontalablation'] - ): + if gdir.glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: if calibrate_regional_glen_a: # nothing else to do here - already ran inversion when calibrating Glen's A continue @@ -243,12 +224,8 @@ def run( calving_k = calving_df.loc[calving_idx, 'calving_k'] # Otherwise, use region's median value else: - calving_df['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values - ] - calving_df_reg = calving_df.loc[ - calving_df['O1Region'] == int(gdir.rgi_id[6:8]), : - ] + calving_df['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values] + calving_df_reg = calving_df.loc[calving_df['O1Region'] == int(gdir.rgi_id[6:8]), :] calving_k = np.median(calving_df_reg.calving_k) # increase calving line for inversion so that later spinup will work @@ -318,6 +295,18 @@ def main(): default=True, help="If True (False) run ice thickness inversion and regionally calibrate (use previously calibrated or user-input) Glen's A values. Default is True", ) + parser.add_argument( + '-glen_a', + type=float, + default=None, + help="User-selected inversion Glen's creep parameter value", + ) + parser.add_argument( + '-fs', + type=float, + default=None, + help="User-selected inversion Orleam's sliding factor value", + ) parser.add_argument( '-ncores', action='store', @@ -333,6 +322,11 @@ def main(): parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') args = parser.parse_args() + # --- Validation logic --- + if args.calibrate_regional_glen_a: + if args.glen_a is not None or args.fs is not None: + parser.error("When '-calibrate_regional_glen_a' is True, '-glen_a' and '-fs' must both be None.") + # RGI glacier batches if args.rgi_region01: batches = [ @@ -362,6 +356,8 @@ def main(): run, ncores=args.ncores, calibrate_regional_glen_a=args.calibrate_regional_glen_a, + glen_a=args.glen_a, + fs=args.fs, reset_gdirs=args.reset_gdirs, debug=args.debug, ) diff --git a/pygem/bin/run/run_mcmc_priors.py b/pygem/bin/run/run_mcmc_priors.py index 6333fcb3..1741f679 100644 --- a/pygem/bin/run/run_mcmc_priors.py +++ b/pygem/bin/run/run_mcmc_priors.py @@ -95,16 +95,12 @@ def getparser(): '-priors_reg_outpath', action='store', type=str, - default=pygem_prms['root'] - + '/Output/calibration/' - + pygem_prms['calib']['priors_reg_fn'], + default=pygem_prms['root'] + '/Output/calibration/' + pygem_prms['calib']['priors_reg_fn'], help='output path', ) # flags parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') - parser.add_argument( - '-p', '--plot', action='store_true', help='Flag for plotting regional priors' - ) + parser.add_argument('-p', '--plot', action='store_true', help='Flag for plotting regional priors') return parser @@ -113,9 +109,7 @@ def export_priors(priors_df_single, reg, regO2, priors_reg_outpath=''): if os.path.exists(priors_reg_outpath): priors_df = pd.read_csv(priors_reg_outpath) # Add or overwrite existing priors - priors_idx = np.where( - (priors_df.O1Region == reg) & (priors_df.O2Region == regO2) - )[0] + priors_idx = np.where((priors_df.O1Region == reg) & (priors_df.O2Region == regO2))[0] if len(priors_idx) > 0: priors_df.loc[priors_idx, :] = priors_df_single.values else: @@ -132,18 +126,12 @@ def export_priors(priors_df_single, reg, regO2, priors_reg_outpath=''): def plot_hist(main_glac_rgi_subset, fig_fp, reg, regO2=''): # Histograms and record model parameter statistics - fig, ax = plt.subplots( - 1, 2, figsize=(6, 4), gridspec_kw={'wspace': 0.3, 'hspace': 0.3} - ) + fig, ax = plt.subplots(1, 2, figsize=(6, 4), gridspec_kw={'wspace': 0.3, 'hspace': 0.3}) labelsize = 1 fig.text( 0.5, 0.9, - 'Region ' - + str(reg) - + ' (subregion: ' - + str(regO2) - + ')'.replace(' (subregion: )', '(all subregions)'), + 'Region ' + str(reg) + ' (subregion: ' + str(regO2) + ')'.replace(' (subregion: )', '(all subregions)'), ha='center', size=14, ) @@ -166,9 +154,7 @@ def plot_reg_priors(main_glac_rgi, priors_df, reg, rgi_regionsO2, fig_fp): nrows = int(np.ceil(len(rgi_regionsO2) / ncols)) priors_df_regO1 = priors_df.loc[priors_df['O1Region'] == reg] - fig, ax = plt.subplots( - nrows, ncols, squeeze=False, gridspec_kw={'wspace': 0.5, 'hspace': 0.5} - ) + fig, ax = plt.subplots(nrows, ncols, squeeze=False, gridspec_kw={'wspace': 0.5, 'hspace': 0.5}) nrow = 0 ncol = 0 for nreg, regO2 in enumerate(rgi_regionsO2): @@ -256,16 +242,12 @@ def plot_reg_priors(main_glac_rgi, priors_df, reg, rgi_regionsO2, fig_fp): ) # ===== REGIONAL PRIOR: TEMPERATURE BIAS ====== - fig, ax = plt.subplots( - nrows, ncols, squeeze=False, gridspec_kw={'wspace': 0.3, 'hspace': 0.3} - ) + fig, ax = plt.subplots(nrows, ncols, squeeze=False, gridspec_kw={'wspace': 0.3, 'hspace': 0.3}) nrow = 0 ncol = 0 for nreg, regO2 in enumerate(rgi_regionsO2): priors_df_regO2 = priors_df_regO1.loc[priors_df['O2Region'] == regO2] - tbias_values = main_glac_rgi.loc[ - main_glac_rgi['O2Region'] == regO2, 'tbias' - ].values + tbias_values = main_glac_rgi.loc[main_glac_rgi['O2Region'] == regO2, 'tbias'].values nglaciers = tbias_values.shape[0] # Plot histogram @@ -285,13 +267,7 @@ def plot_reg_priors(main_glac_rgi, priors_df, reg, rgi_regionsO2, fig_fp): ax[nrow, ncol].plot(bins, rv.pdf(bins), color='k') # add alpha and beta as text normtext = ( - r'$\mu$=' - + str(np.round(mu, 2)) - + '\n' - + r'$\sigma$=' - + str(np.round(sigma, 2)) - + '\n$n$=' - + str(nglaciers) + r'$\mu$=' + str(np.round(mu, 2)) + '\n' + r'$\sigma$=' + str(np.round(sigma, 2)) + '\n$n$=' + str(nglaciers) ) ax[nrow, ncol].text( 0.98, @@ -348,17 +324,11 @@ def plot_reg_priors(main_glac_rgi, priors_df, reg, rgi_regionsO2, fig_fp): ) -def run( - reg, option_calibration='emulator', priors_reg_outpath='', debug=False, plot=False -): +def run(reg, option_calibration='emulator', priors_reg_outpath='', debug=False, plot=False): # Calibration filepath modelprms_fp = pygem_prms['root'] + '/Output/calibration/' + str(reg).zfill(2) + '/' # Load glaciers - glac_list = [ - x.split('-')[0] - for x in os.listdir(modelprms_fp) - if x.endswith('-modelprms_dict.json') - ] + glac_list = [x.split('-')[0] for x in os.listdir(modelprms_fp) if x.endswith('-modelprms_dict.json')] glac_list = sorted(glac_list) main_glac_rgi = modelsetup.selectglaciersrgitable(glac_no=glac_list) @@ -391,9 +361,7 @@ def run( main_glac_rgi.loc[nglac, 'mb_obs_mwea'] = modelprms['mb_obs_mwea'][0] # get regional difference between calibrated mb_mwea and observed - main_glac_rgi['mb_dif_obs_cal'] = ( - main_glac_rgi['mb_obs_mwea'] - main_glac_rgi['mb_mwea'] - ) + main_glac_rgi['mb_dif_obs_cal'] = main_glac_rgi['mb_obs_mwea'] - main_glac_rgi['mb_mwea'] # define figure output path if plot: @@ -404,9 +372,7 @@ def run( if reg not in [19]: rgi_regionsO2 = np.unique(main_glac_rgi.O2Region.values) for regO2 in rgi_regionsO2: - main_glac_rgi_subset = main_glac_rgi.loc[ - main_glac_rgi['O2Region'] == regO2, : - ] + main_glac_rgi_subset = main_glac_rgi.loc[main_glac_rgi['O2Region'] == regO2, :] if plot: plot_hist(main_glac_rgi_subset, fig_fp, reg, regO2) @@ -456,9 +422,7 @@ def run( ) # export results - priors_df_single = pd.DataFrame( - np.zeros((1, len(priors_cn))), columns=priors_cn - ) + priors_df_single = pd.DataFrame(np.zeros((1, len(priors_cn))), columns=priors_cn) priors_df_single.loc[0, :] = [ reg, regO2, @@ -531,9 +495,7 @@ def run( for regO2 in rgi_regionsO2: # export results - priors_df_single = pd.DataFrame( - np.zeros((1, len(priors_cn))), columns=priors_cn - ) + priors_df_single = pd.DataFrame(np.zeros((1, len(priors_cn))), columns=priors_cn) priors_df_single.loc[0, :] = [ reg, regO2, diff --git a/pygem/bin/run/run_simulation.py b/pygem/bin/run/run_simulation.py index bc5f9f38..aa252914 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -7,9 +7,9 @@ Run a model simulation """ -# Default climate data is ERA-Interim; specify CMIP5 by specifying a filename to the argument: +# Default climate data is ERA5; specify CMIP5 by specifying a filename to the argument: # (Command line) python run_simulation_list_multiprocess.py -gcm_list_fn=C:\...\gcm_rcpXX_filenames.txt -# - Default is running ERA-Interim in parallel with five processors. +# - Default is running ERA5 in parallel with five processors. # (Spyder) %run run_simulation_list_multiprocess.py C:\...\gcm_rcpXX_filenames.txt -option_parallels=0 # - Spyder cannot run parallels, so always set -option_parallels=0 when testing in Spyder. # Spyder cannot run parallels, so always set -option_parallels=0 when testing in Spyder. @@ -41,7 +41,7 @@ # read the config pygem_prms = config_manager.read_config() # oggm imports -from oggm import cfg, graphics, tasks, utils +from oggm import cfg, tasks, utils from oggm.core.flowline import FluxBasedModel, SemiImplicitModel from oggm.core.massbalance import apparent_mb_from_any_mb @@ -55,6 +55,7 @@ single_flowline_glacier_directory_with_calving, ) from pygem.output import calc_stats_array +from pygem.plot import graphics from pygem.shop import debris cfg.PARAMS['hydro_month_nh'] = 1 @@ -365,9 +366,7 @@ def run(list_packed_vars): glac_no = list_packed_vars[1] sim_climate_name = list_packed_vars[2] realization = list_packed_vars[3] - if (sim_climate_name != args.ref_climate_name) and ( - args.sim_climate_scenario is None - ): + if (sim_climate_name != args.ref_climate_name) and (args.sim_climate_scenario is None): sim_climate_scenario = os.path.basename(args.gcm_list_fn).split('_')[1] else: sim_climate_scenario = args.sim_climate_scenario @@ -408,16 +407,14 @@ def run(list_packed_vars): # ===== LOAD CLIMATE DATA ===== # Climate class - if sim_climate_name in ['ERA5', 'ERA-Interim', 'COAWST']: + if sim_climate_name in ['ERA5', 'COAWST']: gcm = class_climate.GCM(name=sim_climate_name) ref_gcm = gcm dates_table_ref = dates_table_full else: # GCM object if realization is None: - gcm = class_climate.GCM( - name=sim_climate_name, sim_climate_scenario=sim_climate_scenario - ) + gcm = class_climate.GCM(name=sim_climate_name, sim_climate_scenario=sim_climate_scenario) else: gcm = class_climate.GCM( name=sim_climate_name, @@ -444,14 +441,10 @@ def run(list_packed_vars): ) # Elevation [m asl] try: - gcm_elev = gcm.importGCMfxnearestneighbor_xarray( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi - ) + gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi) except: gcm_elev = None - ref_elev = ref_gcm.importGCMfxnearestneighbor_xarray( - ref_gcm.elev_fn, ref_gcm.elev_vn, main_glac_rgi - ) + ref_elev = ref_gcm.importGCMfxnearestneighbor_xarray(ref_gcm.elev_fn, ref_gcm.elev_vn, main_glac_rgi) # ----- Temperature and Precipitation Bias Adjustments ----- # No adjustments @@ -480,16 +473,14 @@ def run(list_packed_vars): args.ref_startyear, ) # Precipitation bias correction - gcm_prec_adj, gcm_elev_adj, gcm_prec_biasadj_frac = ( - gcmbiasadj.prec_biasadj_opt1( - ref_prec, - ref_elev, - gcm_prec, - dates_table_ref, - dates_table_full, - args.sim_startyear, - args.ref_startyear, - ) + gcm_prec_adj, gcm_elev_adj, gcm_prec_biasadj_frac = gcmbiasadj.prec_biasadj_opt1( + ref_prec, + ref_elev, + gcm_prec, + dates_table_ref, + dates_table_full, + args.sim_startyear, + args.ref_startyear, ) # OPTION 2: Adjust temp and prec using Huss and Hock (2015) elif args.option_bias_adjustment == 2: @@ -504,14 +495,12 @@ def run(list_packed_vars): args.ref_startyear, ) # Precipitation bias correction - gcm_prec_adj, gcm_elev_adj, gcm_prec_biasadj_frac = ( - gcmbiasadj.prec_biasadj_HH2015( - ref_prec, - ref_elev, - gcm_prec, - dates_table_ref, - dates_table_full, - ) + gcm_prec_adj, gcm_elev_adj, gcm_prec_biasadj_frac = gcmbiasadj.prec_biasadj_HH2015( + ref_prec, + ref_elev, + gcm_prec, + dates_table_ref, + dates_table_full, ) # OPTION 3: Adjust temp and prec using quantile delta mapping, Cannon et al. (2015) elif args.option_bias_adjustment == 3: @@ -561,15 +550,13 @@ def run(list_packed_vars): verbose=debug, ) # Monthly average from reference climate data - gcm_tempstd = gcmbiasadj.monthly_avg_array_rolled( - ref_tempstd, dates_table_ref, dates_table_full - ) + gcm_tempstd = gcmbiasadj.monthly_avg_array_rolled(ref_tempstd, dates_table_ref, dates_table_full) else: gcm_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table.shape[0])) ref_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table_ref.shape[0])) # Lapse rate - if sim_climate_name in ['ERA-Interim', 'ERA5']: + if sim_climate_name == 'ERA5': gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( gcm.lr_fn, gcm.lr_vn, @@ -621,19 +608,12 @@ def run(list_packed_vars): # for batman in [0]: # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== - if ( - glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_frontalablation'] - ): - gdir = single_flowline_glacier_directory( - glacier_str, working_dir=args.oggm_working_dir - ) + if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: + gdir = single_flowline_glacier_directory(glacier_str, working_dir=args.oggm_working_dir) gdir.is_tidewater = False calving_k = None else: - gdir = single_flowline_glacier_directory_with_calving( - glacier_str, working_dir=args.oggm_working_dir - ) + gdir = single_flowline_glacier_directory_with_calving(glacier_str, working_dir=args.oggm_working_dir) gdir.is_tidewater = True cfg.PARAMS['use_kcalving_for_inversion'] = True cfg.PARAMS['use_kcalving_for_run'] = True @@ -659,10 +639,10 @@ def run(list_packed_vars): 'prec': gcm_prec_adj[glac, :], 'lr': gcm_lr[glac, :], } - fact = gcm_prec_biasadj_frac[glac] - if fact < 0.5 or fact > 2: + # Warn if precipitation bias adjustment is greater than 2x + if gcm_prec_biasadj_frac[glac] < 0.5 or gcm_prec_biasadj_frac[glac] > 2: warnings.warn( - f'Bias-adjusted GCM precipitation for {glacier_str} differs from that of the refernce climate data by a factor greater than 2x ({round(fact, 2)})', + f'Bias-adjusted GCM precipitation for {glacier_str} differs from that of the refernce climate data by a factor greater than 2x ({round(gcm_prec_biasadj_frac[glac], 2)})', Warning, stacklevel=2, ) @@ -676,15 +656,10 @@ def run(list_packed_vars): if not modelprms_fp: modelprms_fn = glacier_str + '-modelprms_dict.json' modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' + pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' ) + modelprms_fn - assert os.path.exists(modelprms_fp), ( - 'Calibrated parameters do not exist.' - ) + assert os.path.exists(modelprms_fp), 'Calibrated parameters do not exist.' with open(modelprms_fp, 'r') as f: modelprms_dict = json.load(f) @@ -698,12 +673,8 @@ def run(list_packed_vars): modelprms_all = { 'kp': [np.median(modelprms_all['kp']['chain_0'])], 'tbias': [np.median(modelprms_all['tbias']['chain_0'])], - 'ddfsnow': [ - np.median(modelprms_all['ddfsnow']['chain_0']) - ], - 'ddfice': [ - np.median(modelprms_all['ddfice']['chain_0']) - ], + 'ddfsnow': [np.median(modelprms_all['ddfsnow']['chain_0'])], + 'ddfice': [np.median(modelprms_all['ddfice']['chain_0'])], 'tsnow_threshold': modelprms_all['tsnow_threshold'], 'precgrad': modelprms_all['precgrad'], } @@ -715,28 +686,13 @@ def run(list_packed_vars): mp_idx_start = np.arange(sims_burn, sims_burn + mp_spacing) np.random.shuffle(mp_idx_start) mp_idx_start = mp_idx_start[0] - mp_idx_all = np.arange( - mp_idx_start, mcmc_sample_no, mp_spacing - ) + mp_idx_all = np.arange(mp_idx_start, mcmc_sample_no, mp_spacing) modelprms_all = { - 'kp': [ - modelprms_all['kp']['chain_0'][mp_idx] - for mp_idx in mp_idx_all - ], - 'tbias': [ - modelprms_all['tbias']['chain_0'][mp_idx] - for mp_idx in mp_idx_all - ], - 'ddfsnow': [ - modelprms_all['ddfsnow']['chain_0'][mp_idx] - for mp_idx in mp_idx_all - ], - 'ddfice': [ - modelprms_all['ddfice']['chain_0'][mp_idx] - for mp_idx in mp_idx_all - ], - 'tsnow_threshold': modelprms_all['tsnow_threshold'] - * nsims, + 'kp': [modelprms_all['kp']['chain_0'][mp_idx] for mp_idx in mp_idx_all], + 'tbias': [modelprms_all['tbias']['chain_0'][mp_idx] for mp_idx in mp_idx_all], + 'ddfsnow': [modelprms_all['ddfsnow']['chain_0'][mp_idx] for mp_idx in mp_idx_all], + 'ddfice': [modelprms_all['ddfice']['chain_0'][mp_idx] for mp_idx in mp_idx_all], + 'tsnow_threshold': modelprms_all['tsnow_threshold'] * nsims, 'precgrad': modelprms_all['precgrad'] * nsims, } else: @@ -751,9 +707,7 @@ def run(list_packed_vars): else: # Load quality controlled frontal ablation data fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' - assert os.path.exists(fp), ( - 'Calibrated calving dataset does not exist' - ) + assert os.path.exists(fp), 'Calibrated calving dataset does not exist' calving_df = pd.read_csv(fp) calving_rgiids = list(calving_df.RGIId) @@ -761,46 +715,35 @@ def run(list_packed_vars): if rgiid in calving_rgiids: calving_idx = calving_rgiids.index(rgiid) calving_k = calving_df.loc[calving_idx, 'calving_k'] - calving_k_nmad = calving_df.loc[ - calving_idx, 'calving_k_nmad' - ] + calving_k_nmad = calving_df.loc[calving_idx, 'calving_k_nmad'] # Otherwise, use region's median value else: calving_df['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) - for x in calving_df.RGIId.values - ] - calving_df_reg = calving_df.loc[ - calving_df['O1Region'] == int(reg_str), : + int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values ] + calving_df_reg = calving_df.loc[calving_df['O1Region'] == int(reg_str), :] calving_k = np.median(calving_df_reg.calving_k) calving_k_nmad = 0 if nsims == 1: calving_k_values = np.array([calving_k]) else: - calving_k_values = calving_k + np.random.normal( - loc=0, scale=calving_k_nmad, size=nsims - ) + calving_k_values = calving_k + np.random.normal(loc=0, scale=calving_k_nmad, size=nsims) calving_k_values[calving_k_values < 0.001] = 0.001 calving_k_values[calving_k_values > 5] = 5 # calving_k_values[:] = calving_k - while ( - not abs(np.median(calving_k_values) - calving_k) < 0.001 - ): - calving_k_values = calving_k + np.random.normal( - loc=0, scale=calving_k_nmad, size=nsims - ) + while not abs(np.median(calving_k_values) - calving_k) < 0.001: + calving_k_values = calving_k + np.random.normal(loc=0, scale=calving_k_nmad, size=nsims) calving_k_values[calving_k_values < 0.001] = 0.001 calving_k_values[calving_k_values > 5] = 5 # print(calving_k, np.median(calving_k_values)) - assert ( - abs(np.median(calving_k_values) - calving_k) < 0.001 - ), 'calving_k distribution too far off' + assert abs(np.median(calving_k_values) - calving_k) < 0.001, ( + 'calving_k distribution too far off' + ) if debug: print( @@ -816,18 +759,11 @@ def run(list_packed_vars): 'kp': [args.kp], 'tbias': [args.tbias], 'ddfsnow': [args.ddfsnow], - 'ddfice': [ - args.ddfsnow - / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ], - 'tsnow_threshold': [ - pygem_prms['sim']['params']['tsnow_threshold'] - ], + 'ddfice': [args.ddfsnow / pygem_prms['sim']['params']['ddfsnow_iceratio']], + 'tsnow_threshold': [pygem_prms['sim']['params']['tsnow_threshold']], 'precgrad': [pygem_prms['sim']['params']['precgrad']], } - calving_k = ( - np.zeros(nsims) + pygem_prms['sim']['params']['calving_k'] - ) + calving_k = np.zeros(nsims) + pygem_prms['sim']['params']['calving_k'] calving_k_values = calving_k if debug and gdir.is_tidewater: @@ -840,13 +776,9 @@ def run(list_packed_vars): glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation'] ): - cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics'][ - 'cfl_number' - ] + cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics']['cfl_number'] else: - cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics'][ - 'cfl_number_calving' - ] + cfg.PARAMS['cfl_number'] = pygem_prms['sim']['oggm_dynamics']['cfl_number_calving'] if debug: print('cfl number:', cfg.PARAMS['cfl_number']) @@ -856,22 +788,14 @@ def run(list_packed_vars): f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}' ) glena_O1regions = [int(x) for x in glena_df.O1Region.values] - assert glacier_rgi_table.O1Region in glena_O1regions, ( - glacier_str + ' O1 region not in glena_df' - ) - glena_idx = np.where( - glena_O1regions == glacier_rgi_table.O1Region - )[0][0] - glen_a_multiplier = glena_df.loc[ - glena_idx, 'glens_a_multiplier' - ] + assert glacier_rgi_table.O1Region in glena_O1regions, glacier_str + ' O1 region not in glena_df' + glena_idx = np.where(glena_O1regions == glacier_rgi_table.O1Region)[0][0] + glen_a_multiplier = glena_df.loc[glena_idx, 'glens_a_multiplier'] fs = glena_df.loc[glena_idx, 'fs'] else: args.option_dynamics = None fs = pygem_prms['sim']['oggm_dynamics']['fs'] - glen_a_multiplier = pygem_prms['sim']['oggm_dynamics'][ - 'glen_a_multiplier' - ] + glen_a_multiplier = pygem_prms['sim']['oggm_dynamics']['glen_a_multiplier'] glen_a = cfg.PARAMS['glen_a'] * glen_a_multiplier # spinup @@ -889,75 +813,31 @@ def run(list_packed_vars): # Time attributes and values if pygem_prms['climate']['sim_wateryear'] == 'hydro': - annual_columns = np.unique(dates_table['wateryear'].values)[ - 0 : int(dates_table.shape[0] / 12) - ] + annual_columns = np.unique(dates_table['wateryear'].values)[0 : int(dates_table.shape[0] / 12)] else: - annual_columns = np.unique(dates_table['year'].values)[ - 0 : int(dates_table.shape[0] / 12) - ] + annual_columns = np.unique(dates_table['year'].values)[0 : int(dates_table.shape[0] / 12)] # append additional year to year_values to account for mass and area at end of period year_values = annual_columns - year_values = np.concatenate( - (year_values, np.array([annual_columns[-1] + 1])) - ) - output_glac_temp_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_prec_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_acc_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_refreeze_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_melt_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_frontalablation_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_massbaltotal_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_runoff_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_snowline_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_glac_area_annual = ( - np.zeros((year_values.shape[0], nsims)) * np.nan - ) - output_glac_mass_annual = ( - np.zeros((year_values.shape[0], nsims)) * np.nan - ) - output_glac_mass_bsl_annual = ( - np.zeros((year_values.shape[0], nsims)) * np.nan - ) - output_glac_mass_change_ignored_annual = np.zeros( - (year_values.shape[0], nsims) - ) - output_glac_ELA_annual = ( - np.zeros((year_values.shape[0], nsims)) * np.nan - ) - output_offglac_prec_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_offglac_refreeze_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_offglac_melt_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_offglac_snowpack_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) - output_offglac_runoff_monthly = ( - np.zeros((dates_table.shape[0], nsims)) * np.nan - ) + year_values = np.concatenate((year_values, np.array([annual_columns[-1] + 1]))) + output_glac_temp_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_prec_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_acc_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_refreeze_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_melt_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_frontalablation_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_massbaltotal_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_runoff_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_snowline_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_area_annual = np.zeros((year_values.shape[0], nsims)) * np.nan + output_glac_mass_annual = np.zeros((year_values.shape[0], nsims)) * np.nan + output_glac_mass_bsl_annual = np.zeros((year_values.shape[0], nsims)) * np.nan + output_glac_mass_change_ignored_annual = np.zeros((year_values.shape[0], nsims)) + output_glac_ELA_annual = np.zeros((year_values.shape[0], nsims)) * np.nan + output_offglac_prec_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_refreeze_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_melt_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_snowpack_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_runoff_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan output_glac_bin_icethickness_annual = None # Loop through model parameters @@ -1004,7 +884,8 @@ def run(list_packed_vars): else: inversion_filter = False - # run inversion + # run inversion if spinup was not run previously + # note that if `args.spinup`, spinup flowlines have already been imported above as `nfls` and thus inversion is not redone here if not args.spinup: # Perform inversion based on PyGEM MB using reference directory mbmod_inv = PyGEMMassBalance( @@ -1017,10 +898,7 @@ def run(list_packed_vars): ) # Non-tidewater glaciers - if ( - not gdir.is_tidewater - or not pygem_prms['setup']['include_frontalablation'] - ): + if not gdir.is_tidewater or not pygem_prms['setup']['include_frontalablation']: # Arbitrariliy shift the MB profile up (or down) until mass balance is zero (equilibrium for inversion) apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) tasks.prepare_for_inversion(gdir) @@ -1109,42 +987,31 @@ def run(list_packed_vars): ) if debug: - graphics.plot_modeloutput_section(ev_model) - plt.show() + fig, ax = plt.subplots(1) + graphics.plot_modeloutput_section(ev_model, ax=ax) try: diag = ev_model.run_until_and_store(args.sim_endyear + 1) - ev_model.mb_model.glac_wide_volume_annual[-1] = ( - diag.volume_m3[-1] - ) - ev_model.mb_model.glac_wide_area_annual[-1] = diag.area_m2[ - -1 - ] + ev_model.mb_model.glac_wide_volume_annual[-1] = diag.volume_m3[-1] + ev_model.mb_model.glac_wide_area_annual[-1] = diag.area_m2[-1] # Record frontal ablation for tidewater glaciers and update total mass balance if gdir.is_tidewater: # Glacier-wide frontal ablation (m3 w.e.) # - note: diag.calving_m3 is cumulative calving if debug: - print( - '\n\ndiag.calving_m3:', diag.calving_m3.values - ) + print('\n\ndiag.calving_m3:', diag.calving_m3.values) print( 'calving_m3_since_y0:', ev_model.calving_m3_since_y0, ) calving_m3_annual = ( - ( - diag.calving_m3.values[1:] - - diag.calving_m3.values[0:-1] - ) + (diag.calving_m3.values[1:] - diag.calving_m3.values[0:-1]) * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] ) for n in np.arange(calving_m3_annual.shape[0]): - ev_model.mb_model.glac_wide_frontalablation[ - 12 * n + 11 - ] = calving_m3_annual[n] + ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3_annual[n] # Glacier-wide total mass balance (m3 w.e.) ev_model.mb_model.glac_wide_massbaltotal = ( @@ -1160,9 +1027,7 @@ def run(list_packed_vars): print( 'avg frontal ablation [Gta]:', np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() - / 1e9 - / nyears, + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, 4, ), ) @@ -1192,7 +1057,6 @@ def run(list_packed_vars): + '/' ) if sim_climate_name not in [ - 'ERA-Interim', 'ERA5', 'COAWST', ]: @@ -1200,9 +1064,7 @@ def run(list_packed_vars): if not os.path.exists(fail_domain_fp): os.makedirs(fail_domain_fp, exist_ok=True) txt_fn_fail = glacier_str + '-sim_failed.txt' - with open( - fail_domain_fp + txt_fn_fail, 'w' - ) as text_file: + with open(fail_domain_fp + txt_fn_fail, 'w') as text_file: text_file.write( glacier_str + ' failed to complete ' @@ -1211,9 +1073,7 @@ def run(list_packed_vars): ) elif gdir.is_tidewater: if debug: - print( - 'OGGM dynamics failed, using mass redistribution curves' - ) + print('OGGM dynamics failed, using mass redistribution curves') # Mass redistribution curves glacier dynamics model ev_model = MassRedistributionCurveModel( nfls, @@ -1224,15 +1084,9 @@ def run(list_packed_vars): is_tidewater=gdir.is_tidewater, water_level=water_level, ) - _, diag = ev_model.run_until_and_store( - args.sim_endyear + 1 - ) - ev_model.mb_model.glac_wide_volume_annual = ( - diag.volume_m3.values - ) - ev_model.mb_model.glac_wide_area_annual = ( - diag.area_m2.values - ) + _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) + ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values + ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values # Record frontal ablation for tidewater glaciers and update total mass balance # Update glacier-wide frontal ablation (m3 w.e.) @@ -1249,9 +1103,7 @@ def run(list_packed_vars): print( 'avg frontal ablation [Gta]:', np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() - / 1e9 - / nyears, + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, 4, ), ) @@ -1269,9 +1121,7 @@ def run(list_packed_vars): except: if gdir.is_tidewater: if debug: - print( - 'OGGM dynamics failed, using mass redistribution curves' - ) + print('OGGM dynamics failed, using mass redistribution curves') # Mass redistribution curves glacier dynamics model ev_model = MassRedistributionCurveModel( nfls, @@ -1282,15 +1132,9 @@ def run(list_packed_vars): is_tidewater=gdir.is_tidewater, water_level=water_level, ) - _, diag = ev_model.run_until_and_store( - args.sim_endyear + 1 - ) - ev_model.mb_model.glac_wide_volume_annual = ( - diag.volume_m3.values - ) - ev_model.mb_model.glac_wide_area_annual = ( - diag.area_m2.values - ) + _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) + ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values + ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values # Record frontal ablation for tidewater glaciers and update total mass balance # Update glacier-wide frontal ablation (m3 w.e.) @@ -1307,9 +1151,7 @@ def run(list_packed_vars): print( 'avg frontal ablation [Gta]:', np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() - / 1e9 - / nyears, + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, 4, ), ) @@ -1343,18 +1185,13 @@ def run(list_packed_vars): ) if debug: - print('New glacier vol', ev_model.volume_m3) - graphics.plot_modeloutput_section(ev_model) - plt.show() + fig, ax = plt.subplots(1) + graphics.plot_modeloutput_section(ev_model, ax=ax) try: _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) # print('shape of volume:', ev_model.mb_model.glac_wide_volume_annual.shape, diag.volume_m3.shape) - ev_model.mb_model.glac_wide_volume_annual = ( - diag.volume_m3.values - ) - ev_model.mb_model.glac_wide_area_annual = ( - diag.area_m2.values - ) + ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values + ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values # Record frontal ablation for tidewater glaciers and update total mass balance if gdir.is_tidewater: @@ -1372,9 +1209,7 @@ def run(list_packed_vars): print( 'avg frontal ablation [Gta]:', np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() - / 1e9 - / nyears, + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, 4, ), ) @@ -1404,7 +1239,6 @@ def run(list_packed_vars): + '/' ) if sim_climate_name not in [ - 'ERA-Interim', 'ERA5', 'COAWST', ]: @@ -1412,9 +1246,7 @@ def run(list_packed_vars): if not os.path.exists(fail_domain_fp): os.makedirs(fail_domain_fp, exist_ok=True) txt_fn_fail = glacier_str + '-sim_failed.txt' - with open( - fail_domain_fp + txt_fn_fail, 'w' - ) as text_file: + with open(fail_domain_fp + txt_fn_fail, 'w') as text_file: text_file.write( glacier_str + ' failed to complete ' @@ -1459,9 +1291,7 @@ def run(list_packed_vars): ).sum() / mbmod.glacier_area_initial.sum() mb_all.append(glac_wide_mb_mwea) mbmod.glac_wide_area_annual[-1] = mbmod.glac_wide_area_annual[0] - mbmod.glac_wide_volume_annual[-1] = ( - mbmod.glac_wide_volume_annual[0] - ) + mbmod.glac_wide_volume_annual[-1] = mbmod.glac_wide_volume_annual[0] diag['area_m2'] = mbmod.glac_wide_area_annual diag['volume_m3'] = mbmod.glac_wide_volume_annual diag['volume_bsl_m3'] = 0 @@ -1485,8 +1315,7 @@ def run(list_packed_vars): if successful_run: if args.option_dynamics is not None: if debug: - graphics.plot_modeloutput_section(ev_model) - # graphics.plot_modeloutput_map(gdir, model=ev_model) + graphics.plot_modeloutput_section(ev_model, ax=ax, srfls='--') plt.figure() diag.volume_m3.plot() plt.show() @@ -1501,16 +1330,10 @@ def run(list_packed_vars): * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] ) - mb_mwea_mbmod = ( - mbmod.glac_wide_massbaltotal.sum() - / area_initial - / nyears - ) + mb_mwea_mbmod = mbmod.glac_wide_massbaltotal.sum() / area_initial / nyears if debug: - vol_change_diag = ( - diag.volume_m3.values[-1] - diag.volume_m3.values[0] - ) + vol_change_diag = diag.volume_m3.values[-1] - diag.volume_m3.values[0] print( ' vol init [Gt]:', np.round(diag.volume_m3.values[0] * 0.9 / 1e9, 5), @@ -1539,59 +1362,40 @@ def run(list_packed_vars): output_glac_temp_monthly[:, n_iter] = mbmod.glac_wide_temp output_glac_prec_monthly[:, n_iter] = mbmod.glac_wide_prec output_glac_acc_monthly[:, n_iter] = mbmod.glac_wide_acc - output_glac_refreeze_monthly[:, n_iter] = ( - mbmod.glac_wide_refreeze - ) + output_glac_refreeze_monthly[:, n_iter] = mbmod.glac_wide_refreeze output_glac_melt_monthly[:, n_iter] = mbmod.glac_wide_melt - output_glac_frontalablation_monthly[:, n_iter] = ( - mbmod.glac_wide_frontalablation - ) - output_glac_massbaltotal_monthly[:, n_iter] = ( - mbmod.glac_wide_massbaltotal - ) + output_glac_frontalablation_monthly[:, n_iter] = mbmod.glac_wide_frontalablation + output_glac_massbaltotal_monthly[:, n_iter] = mbmod.glac_wide_massbaltotal output_glac_runoff_monthly[:, n_iter] = mbmod.glac_wide_runoff - output_glac_snowline_monthly[:, n_iter] = ( - mbmod.glac_wide_snowline - ) + output_glac_snowline_monthly[:, n_iter] = mbmod.glac_wide_snowline output_glac_area_annual[:, n_iter] = diag.area_m2.values output_glac_mass_annual[:, n_iter] = ( - diag.volume_m3.values - * pygem_prms['constants']['density_ice'] + diag.volume_m3.values * pygem_prms['constants']['density_ice'] ) output_glac_mass_bsl_annual[:, n_iter] = ( - diag.volume_bsl_m3.values - * pygem_prms['constants']['density_ice'] + diag.volume_bsl_m3.values * pygem_prms['constants']['density_ice'] ) output_glac_mass_change_ignored_annual[:-1, n_iter] = ( - mbmod.glac_wide_volume_change_ignored_annual - * pygem_prms['constants']['density_ice'] + mbmod.glac_wide_volume_change_ignored_annual * pygem_prms['constants']['density_ice'] ) output_glac_ELA_annual[:, n_iter] = mbmod.glac_wide_ELA_annual output_offglac_prec_monthly[:, n_iter] = mbmod.offglac_wide_prec - output_offglac_refreeze_monthly[:, n_iter] = ( - mbmod.offglac_wide_refreeze - ) + output_offglac_refreeze_monthly[:, n_iter] = mbmod.offglac_wide_refreeze output_offglac_melt_monthly[:, n_iter] = mbmod.offglac_wide_melt - output_offglac_snowpack_monthly[:, n_iter] = ( - mbmod.offglac_wide_snowpack - ) - output_offglac_runoff_monthly[:, n_iter] = ( - mbmod.offglac_wide_runoff - ) + output_offglac_snowpack_monthly[:, n_iter] = mbmod.offglac_wide_snowpack + output_offglac_runoff_monthly[:, n_iter] = mbmod.offglac_wide_runoff if output_glac_bin_icethickness_annual is None: - output_glac_bin_area_annual_sim = ( - mbmod.glac_bin_area_annual[:, :, np.newaxis] - ) + output_glac_bin_area_annual_sim = mbmod.glac_bin_area_annual[:, :, np.newaxis] output_glac_bin_mass_annual_sim = ( mbmod.glac_bin_area_annual * mbmod.glac_bin_icethickness_annual * pygem_prms['constants']['density_ice'] )[:, :, np.newaxis] - output_glac_bin_icethickness_annual_sim = ( - mbmod.glac_bin_icethickness_annual - )[:, :, np.newaxis] + output_glac_bin_icethickness_annual_sim = (mbmod.glac_bin_icethickness_annual)[ + :, :, np.newaxis + ] # Update the latest thickness and volume if ev_model is not None: fl_dx_meter = getattr(ev_model.fls[0], 'dx_meter', None) @@ -1605,89 +1409,51 @@ def run(list_packed_vars): # thickness icethickness_t0 = np.zeros(fl_section.shape) icethickness_t0[fl_widths_m > 0] = ( - fl_section[fl_widths_m > 0] - / fl_widths_m[fl_widths_m > 0] - ) - output_glac_bin_icethickness_annual_sim[:, -1, 0] = ( - icethickness_t0 + fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] ) + output_glac_bin_icethickness_annual_sim[:, -1, 0] = icethickness_t0 # mass - glacier_vol_t0 = ( - fl_widths_m * fl_dx_meter * icethickness_t0 - ) + glacier_vol_t0 = fl_widths_m * fl_dx_meter * icethickness_t0 output_glac_bin_mass_annual_sim[:, -1, 0] = ( - glacier_vol_t0 - * pygem_prms['constants']['density_ice'] + glacier_vol_t0 * pygem_prms['constants']['density_ice'] ) - output_glac_bin_area_annual = ( - output_glac_bin_area_annual_sim - ) - output_glac_bin_mass_annual = ( - output_glac_bin_mass_annual_sim - ) - output_glac_bin_icethickness_annual = ( - output_glac_bin_icethickness_annual_sim - ) - output_glac_bin_massbalclim_annual_sim = np.zeros( - mbmod.glac_bin_icethickness_annual.shape - ) - output_glac_bin_massbalclim_annual_sim[:, :-1] = ( - mbmod.glac_bin_massbalclim_annual - ) - output_glac_bin_massbalclim_annual = ( - output_glac_bin_massbalclim_annual_sim[:, :, np.newaxis] - ) - output_glac_bin_massbalclim_monthly_sim = np.zeros( - mbmod.glac_bin_massbalclim.shape - ) - output_glac_bin_massbalclim_monthly_sim = ( - mbmod.glac_bin_massbalclim - ) - output_glac_bin_massbalclim_monthly = ( - output_glac_bin_massbalclim_monthly_sim[ - :, :, np.newaxis - ] - ) + output_glac_bin_area_annual = output_glac_bin_area_annual_sim + output_glac_bin_mass_annual = output_glac_bin_mass_annual_sim + output_glac_bin_icethickness_annual = output_glac_bin_icethickness_annual_sim + output_glac_bin_massbalclim_annual_sim = np.zeros(mbmod.glac_bin_icethickness_annual.shape) + output_glac_bin_massbalclim_annual_sim[:, :-1] = mbmod.glac_bin_massbalclim_annual + output_glac_bin_massbalclim_annual = output_glac_bin_massbalclim_annual_sim[ + :, :, np.newaxis + ] + output_glac_bin_massbalclim_monthly_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) + output_glac_bin_massbalclim_monthly_sim = mbmod.glac_bin_massbalclim + output_glac_bin_massbalclim_monthly = output_glac_bin_massbalclim_monthly_sim[ + :, :, np.newaxis + ] # accum - output_glac_bin_acc_monthly_sim = np.zeros( - mbmod.bin_acc.shape - ) + output_glac_bin_acc_monthly_sim = np.zeros(mbmod.bin_acc.shape) output_glac_bin_acc_monthly_sim = mbmod.bin_acc - output_glac_bin_acc_monthly = ( - output_glac_bin_acc_monthly_sim[:, :, np.newaxis] - ) + output_glac_bin_acc_monthly = output_glac_bin_acc_monthly_sim[:, :, np.newaxis] # refreeze - output_glac_bin_refreeze_monthly_sim = np.zeros( - mbmod.glac_bin_refreeze.shape - ) - output_glac_bin_refreeze_monthly_sim = ( - mbmod.glac_bin_refreeze - ) - output_glac_bin_refreeze_monthly = ( - output_glac_bin_refreeze_monthly_sim[:, :, np.newaxis] - ) + output_glac_bin_refreeze_monthly_sim = np.zeros(mbmod.glac_bin_refreeze.shape) + output_glac_bin_refreeze_monthly_sim = mbmod.glac_bin_refreeze + output_glac_bin_refreeze_monthly = output_glac_bin_refreeze_monthly_sim[:, :, np.newaxis] # melt - output_glac_bin_melt_monthly_sim = np.zeros( - mbmod.glac_bin_melt.shape - ) + output_glac_bin_melt_monthly_sim = np.zeros(mbmod.glac_bin_melt.shape) output_glac_bin_melt_monthly_sim = mbmod.glac_bin_melt - output_glac_bin_melt_monthly = ( - output_glac_bin_melt_monthly_sim[:, :, np.newaxis] - ) + output_glac_bin_melt_monthly = output_glac_bin_melt_monthly_sim[:, :, np.newaxis] else: # Update the latest thickness and volume - output_glac_bin_area_annual_sim = ( - mbmod.glac_bin_area_annual[:, :, np.newaxis] - ) + output_glac_bin_area_annual_sim = mbmod.glac_bin_area_annual[:, :, np.newaxis] output_glac_bin_mass_annual_sim = ( mbmod.glac_bin_area_annual * mbmod.glac_bin_icethickness_annual * pygem_prms['constants']['density_ice'] )[:, :, np.newaxis] - output_glac_bin_icethickness_annual_sim = ( - mbmod.glac_bin_icethickness_annual - )[:, :, np.newaxis] + output_glac_bin_icethickness_annual_sim = (mbmod.glac_bin_icethickness_annual)[ + :, :, np.newaxis + ] if ev_model is not None: fl_dx_meter = getattr(ev_model.fls[0], 'dx_meter', None) fl_widths_m = getattr(ev_model.fls[0], 'widths_m', None) @@ -1700,19 +1466,13 @@ def run(list_packed_vars): # thickness icethickness_t0 = np.zeros(fl_section.shape) icethickness_t0[fl_widths_m > 0] = ( - fl_section[fl_widths_m > 0] - / fl_widths_m[fl_widths_m > 0] - ) - output_glac_bin_icethickness_annual_sim[:, -1, 0] = ( - icethickness_t0 + fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] ) + output_glac_bin_icethickness_annual_sim[:, -1, 0] = icethickness_t0 # mass - glacier_vol_t0 = ( - fl_widths_m * fl_dx_meter * icethickness_t0 - ) + glacier_vol_t0 = fl_widths_m * fl_dx_meter * icethickness_t0 output_glac_bin_mass_annual_sim[:, -1, 0] = ( - glacier_vol_t0 - * pygem_prms['constants']['density_ice'] + glacier_vol_t0 * pygem_prms['constants']['density_ice'] ) output_glac_bin_area_annual = np.append( output_glac_bin_area_annual, @@ -1729,36 +1489,22 @@ def run(list_packed_vars): output_glac_bin_icethickness_annual_sim, axis=2, ) - output_glac_bin_massbalclim_annual_sim = np.zeros( - mbmod.glac_bin_icethickness_annual.shape - ) - output_glac_bin_massbalclim_annual_sim[:, :-1] = ( - mbmod.glac_bin_massbalclim_annual - ) + output_glac_bin_massbalclim_annual_sim = np.zeros(mbmod.glac_bin_icethickness_annual.shape) + output_glac_bin_massbalclim_annual_sim[:, :-1] = mbmod.glac_bin_massbalclim_annual output_glac_bin_massbalclim_annual = np.append( output_glac_bin_massbalclim_annual, - output_glac_bin_massbalclim_annual_sim[ - :, :, np.newaxis - ], + output_glac_bin_massbalclim_annual_sim[:, :, np.newaxis], axis=2, ) - output_glac_bin_massbalclim_monthly_sim = np.zeros( - mbmod.glac_bin_massbalclim.shape - ) - output_glac_bin_massbalclim_monthly_sim = ( - mbmod.glac_bin_massbalclim - ) + output_glac_bin_massbalclim_monthly_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) + output_glac_bin_massbalclim_monthly_sim = mbmod.glac_bin_massbalclim output_glac_bin_massbalclim_monthly = np.append( output_glac_bin_massbalclim_monthly, - output_glac_bin_massbalclim_monthly_sim[ - :, :, np.newaxis - ], + output_glac_bin_massbalclim_monthly_sim[:, :, np.newaxis], axis=2, ) # accum - output_glac_bin_acc_monthly_sim = np.zeros( - mbmod.bin_acc.shape - ) + output_glac_bin_acc_monthly_sim = np.zeros(mbmod.bin_acc.shape) output_glac_bin_acc_monthly_sim = mbmod.bin_acc output_glac_bin_acc_monthly = np.append( output_glac_bin_acc_monthly, @@ -1766,9 +1512,7 @@ def run(list_packed_vars): axis=2, ) # melt - output_glac_bin_melt_monthly_sim = np.zeros( - mbmod.glac_bin_melt.shape - ) + output_glac_bin_melt_monthly_sim = np.zeros(mbmod.glac_bin_melt.shape) output_glac_bin_melt_monthly_sim = mbmod.glac_bin_melt output_glac_bin_melt_monthly = np.append( output_glac_bin_melt_monthly, @@ -1776,12 +1520,8 @@ def run(list_packed_vars): axis=2, ) # refreeze - output_glac_bin_refreeze_monthly_sim = np.zeros( - mbmod.glac_bin_refreeze.shape - ) - output_glac_bin_refreeze_monthly_sim = ( - mbmod.glac_bin_refreeze - ) + output_glac_bin_refreeze_monthly_sim = np.zeros(mbmod.glac_bin_refreeze.shape) + output_glac_bin_refreeze_monthly_sim = mbmod.glac_bin_refreeze output_glac_bin_refreeze_monthly = np.append( output_glac_bin_refreeze_monthly, output_glac_bin_refreeze_monthly_sim[:, :, np.newaxis], @@ -1812,84 +1552,67 @@ def run(list_packed_vars): ) for n_iter in range(nsims): # pass model params for iteration and update output dataset model params - output_stats.set_modelprms( - { - key: modelprms_all[key][n_iter] - for key in modelprms_all - } - ) + output_stats.set_modelprms({key: modelprms_all[key][n_iter] for key in modelprms_all}) # create and return xarray dataset output_stats.create_xr_ds() output_ds_all_stats = output_stats.get_xr_ds() # fill values - output_ds_all_stats['glac_runoff_monthly'].values[0, :] = ( - output_glac_runoff_monthly[:, n_iter] - ) - output_ds_all_stats['glac_area_annual'].values[0, :] = ( - output_glac_area_annual[:, n_iter] - ) - output_ds_all_stats['glac_mass_annual'].values[0, :] = ( - output_glac_mass_annual[:, n_iter] - ) - output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = ( - output_glac_mass_bsl_annual[:, n_iter] - ) - output_ds_all_stats['glac_ELA_annual'].values[0, :] = ( - output_glac_ELA_annual[:, n_iter] - ) - output_ds_all_stats['offglac_runoff_monthly'].values[ - 0, : - ] = output_offglac_runoff_monthly[:, n_iter] + output_ds_all_stats['glac_runoff_monthly'].values[0, :] = output_glac_runoff_monthly[ + :, n_iter + ] + output_ds_all_stats['glac_area_annual'].values[0, :] = output_glac_area_annual[:, n_iter] + output_ds_all_stats['glac_mass_annual'].values[0, :] = output_glac_mass_annual[:, n_iter] + output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = output_glac_mass_bsl_annual[ + :, n_iter + ] + output_ds_all_stats['glac_ELA_annual'].values[0, :] = output_glac_ELA_annual[:, n_iter] + output_ds_all_stats['offglac_runoff_monthly'].values[0, :] = output_offglac_runoff_monthly[ + :, n_iter + ] if args.export_extra_vars: - output_ds_all_stats['glac_temp_monthly'].values[ - 0, : - ] = output_glac_temp_monthly[:, n_iter] + 273.15 - output_ds_all_stats['glac_prec_monthly'].values[ - 0, : - ] = output_glac_prec_monthly[:, n_iter] - output_ds_all_stats['glac_acc_monthly'].values[0, :] = ( - output_glac_acc_monthly[:, n_iter] + output_ds_all_stats['glac_temp_monthly'].values[0, :] = ( + output_glac_temp_monthly[:, n_iter] + 273.15 ) - output_ds_all_stats['glac_refreeze_monthly'].values[ - 0, : - ] = output_glac_refreeze_monthly[:, n_iter] - output_ds_all_stats['glac_melt_monthly'].values[ - 0, : - ] = output_glac_melt_monthly[:, n_iter] - output_ds_all_stats[ - 'glac_frontalablation_monthly' - ].values[0, :] = output_glac_frontalablation_monthly[ + output_ds_all_stats['glac_prec_monthly'].values[0, :] = output_glac_prec_monthly[ :, n_iter ] - output_ds_all_stats['glac_massbaltotal_monthly'].values[ - 0, : - ] = output_glac_massbaltotal_monthly[:, n_iter] - output_ds_all_stats['glac_snowline_monthly'].values[ - 0, : - ] = output_glac_snowline_monthly[:, n_iter] - output_ds_all_stats[ - 'glac_mass_change_ignored_annual' - ].values[0, :] = output_glac_mass_change_ignored_annual[ + output_ds_all_stats['glac_acc_monthly'].values[0, :] = output_glac_acc_monthly[ :, n_iter ] - output_ds_all_stats['offglac_prec_monthly'].values[ - 0, : - ] = output_offglac_prec_monthly[:, n_iter] - output_ds_all_stats['offglac_melt_monthly'].values[ - 0, : - ] = output_offglac_melt_monthly[:, n_iter] - output_ds_all_stats['offglac_refreeze_monthly'].values[ - 0, : - ] = output_offglac_refreeze_monthly[:, n_iter] - output_ds_all_stats['offglac_snowpack_monthly'].values[ - 0, : - ] = output_offglac_snowpack_monthly[:, n_iter] + output_ds_all_stats['glac_refreeze_monthly'].values[0, :] = ( + output_glac_refreeze_monthly[:, n_iter] + ) + output_ds_all_stats['glac_melt_monthly'].values[0, :] = output_glac_melt_monthly[ + :, n_iter + ] + output_ds_all_stats['glac_frontalablation_monthly'].values[0, :] = ( + output_glac_frontalablation_monthly[:, n_iter] + ) + output_ds_all_stats['glac_massbaltotal_monthly'].values[0, :] = ( + output_glac_massbaltotal_monthly[:, n_iter] + ) + output_ds_all_stats['glac_snowline_monthly'].values[0, :] = ( + output_glac_snowline_monthly[:, n_iter] + ) + output_ds_all_stats['glac_mass_change_ignored_annual'].values[0, :] = ( + output_glac_mass_change_ignored_annual[:, n_iter] + ) + output_ds_all_stats['offglac_prec_monthly'].values[0, :] = output_offglac_prec_monthly[ + :, n_iter + ] + output_ds_all_stats['offglac_melt_monthly'].values[0, :] = output_offglac_melt_monthly[ + :, n_iter + ] + output_ds_all_stats['offglac_refreeze_monthly'].values[0, :] = ( + output_offglac_refreeze_monthly[:, n_iter] + ) + output_ds_all_stats['offglac_snowpack_monthly'].values[0, :] = ( + output_offglac_snowpack_monthly[:, n_iter] + ) # export glacierwide stats for iteration output_stats.set_fn( - output_stats.get_fn().replace('SETS', f'set{n_iter}') - + args.outputfn_sfix - + 'all.nc' + output_stats.get_fn().replace('SETS', f'set{n_iter}') + args.outputfn_sfix + 'all.nc' ) output_stats.save_xr_ds() @@ -1915,118 +1638,68 @@ def run(list_packed_vars): output_ds_all_stats = output_stats.get_xr_ds() # get stats from all simulations which will be stored - output_glac_runoff_monthly_stats = calc_stats_array( - output_glac_runoff_monthly - ) - output_glac_area_annual_stats = calc_stats_array( - output_glac_area_annual - ) - output_glac_mass_annual_stats = calc_stats_array( - output_glac_mass_annual - ) - output_glac_mass_bsl_annual_stats = calc_stats_array( - output_glac_mass_bsl_annual - ) - output_glac_ELA_annual_stats = calc_stats_array( - output_glac_ELA_annual - ) - output_offglac_runoff_monthly_stats = calc_stats_array( - output_offglac_runoff_monthly - ) + output_glac_runoff_monthly_stats = calc_stats_array(output_glac_runoff_monthly) + output_glac_area_annual_stats = calc_stats_array(output_glac_area_annual) + output_glac_mass_annual_stats = calc_stats_array(output_glac_mass_annual) + output_glac_mass_bsl_annual_stats = calc_stats_array(output_glac_mass_bsl_annual) + output_glac_ELA_annual_stats = calc_stats_array(output_glac_ELA_annual) + output_offglac_runoff_monthly_stats = calc_stats_array(output_offglac_runoff_monthly) if args.export_extra_vars: - output_glac_temp_monthly_stats = calc_stats_array( - output_glac_temp_monthly - ) - output_glac_prec_monthly_stats = calc_stats_array( - output_glac_prec_monthly - ) - output_glac_acc_monthly_stats = calc_stats_array( - output_glac_acc_monthly - ) - output_glac_refreeze_monthly_stats = calc_stats_array( - output_glac_refreeze_monthly - ) - output_glac_melt_monthly_stats = calc_stats_array( - output_glac_melt_monthly - ) + output_glac_temp_monthly_stats = calc_stats_array(output_glac_temp_monthly) + output_glac_prec_monthly_stats = calc_stats_array(output_glac_prec_monthly) + output_glac_acc_monthly_stats = calc_stats_array(output_glac_acc_monthly) + output_glac_refreeze_monthly_stats = calc_stats_array(output_glac_refreeze_monthly) + output_glac_melt_monthly_stats = calc_stats_array(output_glac_melt_monthly) output_glac_frontalablation_monthly_stats = calc_stats_array( output_glac_frontalablation_monthly ) - output_glac_massbaltotal_monthly_stats = calc_stats_array( - output_glac_massbaltotal_monthly - ) - output_glac_snowline_monthly_stats = calc_stats_array( - output_glac_snowline_monthly - ) + output_glac_massbaltotal_monthly_stats = calc_stats_array(output_glac_massbaltotal_monthly) + output_glac_snowline_monthly_stats = calc_stats_array(output_glac_snowline_monthly) output_glac_mass_change_ignored_annual_stats = calc_stats_array( output_glac_mass_change_ignored_annual ) - output_offglac_prec_monthly_stats = calc_stats_array( - output_offglac_prec_monthly - ) - output_offglac_melt_monthly_stats = calc_stats_array( - output_offglac_melt_monthly - ) - output_offglac_refreeze_monthly_stats = calc_stats_array( - output_offglac_refreeze_monthly - ) - output_offglac_snowpack_monthly_stats = calc_stats_array( - output_offglac_snowpack_monthly - ) + output_offglac_prec_monthly_stats = calc_stats_array(output_offglac_prec_monthly) + output_offglac_melt_monthly_stats = calc_stats_array(output_offglac_melt_monthly) + output_offglac_refreeze_monthly_stats = calc_stats_array(output_offglac_refreeze_monthly) + output_offglac_snowpack_monthly_stats = calc_stats_array(output_offglac_snowpack_monthly) # output mean/median from all simulations - output_ds_all_stats['glac_runoff_monthly'].values[0, :] = ( - output_glac_runoff_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_area_annual'].values[0, :] = ( - output_glac_area_annual_stats[:, 0] - ) - output_ds_all_stats['glac_mass_annual'].values[0, :] = ( - output_glac_mass_annual_stats[:, 0] - ) - output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = ( - output_glac_mass_bsl_annual_stats[:, 0] - ) - output_ds_all_stats['glac_ELA_annual'].values[0, :] = ( - output_glac_ELA_annual_stats[:, 0] - ) - output_ds_all_stats['offglac_runoff_monthly'].values[0, :] = ( - output_offglac_runoff_monthly_stats[:, 0] - ) + output_ds_all_stats['glac_runoff_monthly'].values[0, :] = output_glac_runoff_monthly_stats[:, 0] + output_ds_all_stats['glac_area_annual'].values[0, :] = output_glac_area_annual_stats[:, 0] + output_ds_all_stats['glac_mass_annual'].values[0, :] = output_glac_mass_annual_stats[:, 0] + output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = output_glac_mass_bsl_annual_stats[:, 0] + output_ds_all_stats['glac_ELA_annual'].values[0, :] = output_glac_ELA_annual_stats[:, 0] + output_ds_all_stats['offglac_runoff_monthly'].values[0, :] = output_offglac_runoff_monthly_stats[ + :, 0 + ] if args.export_extra_vars: output_ds_all_stats['glac_temp_monthly'].values[0, :] = ( output_glac_temp_monthly_stats[:, 0] + 273.15 ) - output_ds_all_stats['glac_prec_monthly'].values[0, :] = ( - output_glac_prec_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_acc_monthly'].values[0, :] = ( - output_glac_acc_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_refreeze_monthly'].values[0, :] = ( - output_glac_refreeze_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_melt_monthly'].values[0, :] = ( - output_glac_melt_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_frontalablation_monthly'].values[ - 0, : - ] = output_glac_frontalablation_monthly_stats[:, 0] - output_ds_all_stats['glac_massbaltotal_monthly'].values[ - 0, : - ] = output_glac_massbaltotal_monthly_stats[:, 0] - output_ds_all_stats['glac_snowline_monthly'].values[0, :] = ( - output_glac_snowline_monthly_stats[:, 0] + output_ds_all_stats['glac_prec_monthly'].values[0, :] = output_glac_prec_monthly_stats[:, 0] + output_ds_all_stats['glac_acc_monthly'].values[0, :] = output_glac_acc_monthly_stats[:, 0] + output_ds_all_stats['glac_refreeze_monthly'].values[0, :] = output_glac_refreeze_monthly_stats[ + :, 0 + ] + output_ds_all_stats['glac_melt_monthly'].values[0, :] = output_glac_melt_monthly_stats[:, 0] + output_ds_all_stats['glac_frontalablation_monthly'].values[0, :] = ( + output_glac_frontalablation_monthly_stats[:, 0] ) - output_ds_all_stats['glac_mass_change_ignored_annual'].values[ - 0, : - ] = output_glac_mass_change_ignored_annual_stats[:, 0] - output_ds_all_stats['offglac_prec_monthly'].values[0, :] = ( - output_offglac_prec_monthly_stats[:, 0] + output_ds_all_stats['glac_massbaltotal_monthly'].values[0, :] = ( + output_glac_massbaltotal_monthly_stats[:, 0] ) - output_ds_all_stats['offglac_melt_monthly'].values[0, :] = ( - output_offglac_melt_monthly_stats[:, 0] + output_ds_all_stats['glac_snowline_monthly'].values[0, :] = output_glac_snowline_monthly_stats[ + :, 0 + ] + output_ds_all_stats['glac_mass_change_ignored_annual'].values[0, :] = ( + output_glac_mass_change_ignored_annual_stats[:, 0] ) + output_ds_all_stats['offglac_prec_monthly'].values[0, :] = output_offglac_prec_monthly_stats[ + :, 0 + ] + output_ds_all_stats['offglac_melt_monthly'].values[0, :] = output_offglac_melt_monthly_stats[ + :, 0 + ] output_ds_all_stats['offglac_refreeze_monthly'].values[0, :] = ( output_offglac_refreeze_monthly_stats[:, 0] ) @@ -2036,82 +1709,69 @@ def run(list_packed_vars): # output median absolute deviation if nsims > 1: - output_ds_all_stats['glac_runoff_monthly_mad'].values[0, :] = ( - output_glac_runoff_monthly_stats[:, 1] - ) - output_ds_all_stats['glac_area_annual_mad'].values[0, :] = ( - output_glac_area_annual_stats[:, 1] - ) - output_ds_all_stats['glac_mass_annual_mad'].values[0, :] = ( - output_glac_mass_annual_stats[:, 1] - ) + output_ds_all_stats['glac_runoff_monthly_mad'].values[0, :] = output_glac_runoff_monthly_stats[ + :, 1 + ] + output_ds_all_stats['glac_area_annual_mad'].values[0, :] = output_glac_area_annual_stats[:, 1] + output_ds_all_stats['glac_mass_annual_mad'].values[0, :] = output_glac_mass_annual_stats[:, 1] output_ds_all_stats['glac_mass_bsl_annual_mad'].values[0, :] = ( output_glac_mass_bsl_annual_stats[:, 1] ) - output_ds_all_stats['glac_ELA_annual_mad'].values[0, :] = ( - output_glac_ELA_annual_stats[:, 1] + output_ds_all_stats['glac_ELA_annual_mad'].values[0, :] = output_glac_ELA_annual_stats[:, 1] + output_ds_all_stats['offglac_runoff_monthly_mad'].values[0, :] = ( + output_offglac_runoff_monthly_stats[:, 1] ) - output_ds_all_stats['offglac_runoff_monthly_mad'].values[ - 0, : - ] = output_offglac_runoff_monthly_stats[:, 1] if args.export_extra_vars: - output_ds_all_stats['glac_temp_monthly_mad'].values[ - 0, : - ] = output_glac_temp_monthly_stats[:, 1] - output_ds_all_stats['glac_prec_monthly_mad'].values[ - 0, : - ] = output_glac_prec_monthly_stats[:, 1] - output_ds_all_stats['glac_acc_monthly_mad'].values[0, :] = ( - output_glac_acc_monthly_stats[:, 1] + output_ds_all_stats['glac_temp_monthly_mad'].values[0, :] = output_glac_temp_monthly_stats[ + :, 1 + ] + output_ds_all_stats['glac_prec_monthly_mad'].values[0, :] = output_glac_prec_monthly_stats[ + :, 1 + ] + output_ds_all_stats['glac_acc_monthly_mad'].values[0, :] = output_glac_acc_monthly_stats[ + :, 1 + ] + output_ds_all_stats['glac_refreeze_monthly_mad'].values[0, :] = ( + output_glac_refreeze_monthly_stats[:, 1] ) - output_ds_all_stats['glac_refreeze_monthly_mad'].values[ - 0, : - ] = output_glac_refreeze_monthly_stats[:, 1] - output_ds_all_stats['glac_melt_monthly_mad'].values[ - 0, : - ] = output_glac_melt_monthly_stats[:, 1] - output_ds_all_stats[ - 'glac_frontalablation_monthly_mad' - ].values[0, :] = output_glac_frontalablation_monthly_stats[ + output_ds_all_stats['glac_melt_monthly_mad'].values[0, :] = output_glac_melt_monthly_stats[ :, 1 ] - output_ds_all_stats['glac_massbaltotal_monthly_mad'].values[ - 0, : - ] = output_glac_massbaltotal_monthly_stats[:, 1] - output_ds_all_stats['glac_snowline_monthly_mad'].values[ - 0, : - ] = output_glac_snowline_monthly_stats[:, 1] - output_ds_all_stats[ - 'glac_mass_change_ignored_annual_mad' - ].values[ - 0, : - ] = output_glac_mass_change_ignored_annual_stats[:, 1] - output_ds_all_stats['offglac_prec_monthly_mad'].values[ - 0, : - ] = output_offglac_prec_monthly_stats[:, 1] - output_ds_all_stats['offglac_melt_monthly_mad'].values[ - 0, : - ] = output_offglac_melt_monthly_stats[:, 1] - output_ds_all_stats['offglac_refreeze_monthly_mad'].values[ - 0, : - ] = output_offglac_refreeze_monthly_stats[:, 1] - output_ds_all_stats['offglac_snowpack_monthly_mad'].values[ - 0, : - ] = output_offglac_snowpack_monthly_stats[:, 1] + output_ds_all_stats['glac_frontalablation_monthly_mad'].values[0, :] = ( + output_glac_frontalablation_monthly_stats[:, 1] + ) + output_ds_all_stats['glac_massbaltotal_monthly_mad'].values[0, :] = ( + output_glac_massbaltotal_monthly_stats[:, 1] + ) + output_ds_all_stats['glac_snowline_monthly_mad'].values[0, :] = ( + output_glac_snowline_monthly_stats[:, 1] + ) + output_ds_all_stats['glac_mass_change_ignored_annual_mad'].values[0, :] = ( + output_glac_mass_change_ignored_annual_stats[:, 1] + ) + output_ds_all_stats['offglac_prec_monthly_mad'].values[0, :] = ( + output_offglac_prec_monthly_stats[:, 1] + ) + output_ds_all_stats['offglac_melt_monthly_mad'].values[0, :] = ( + output_offglac_melt_monthly_stats[:, 1] + ) + output_ds_all_stats['offglac_refreeze_monthly_mad'].values[0, :] = ( + output_offglac_refreeze_monthly_stats[:, 1] + ) + output_ds_all_stats['offglac_snowpack_monthly_mad'].values[0, :] = ( + output_offglac_snowpack_monthly_stats[:, 1] + ) # export merged netcdf glacierwide stats output_stats.set_fn( - output_stats.get_fn().replace('SETS', f'{nsims}sets') - + args.outputfn_sfix - + 'all.nc' + output_stats.get_fn().replace('SETS', f'{nsims}sets') + args.outputfn_sfix + 'all.nc' ) output_stats.save_xr_ds() # ----- DECADAL ICE THICKNESS STATS FOR OVERDEEPENINGS ----- if ( args.export_binned_data - and glacier_rgi_table.Area - > pygem_prms['sim']['out']['export_binned_area_threshold'] + and glacier_rgi_table.Area > pygem_prms['sim']['out']['export_binned_area_threshold'] ): # Distance from top of glacier downglacier output_glac_bin_dist = np.arange(nfls[0].nx) * nfls[0].dx_meter @@ -2137,59 +1797,42 @@ def run(list_packed_vars): ) for n_iter in range(nsims): # pass model params for iteration and update output dataset model params - output_binned.set_modelprms( - { - key: modelprms_all[key][n_iter] - for key in modelprms_all - } - ) + output_binned.set_modelprms({key: modelprms_all[key][n_iter] for key in modelprms_all}) # create and return xarray dataset output_binned.create_xr_ds() output_ds_binned_stats = output_binned.get_xr_ds() # fill values - output_ds_binned_stats['bin_distance'].values[0, :] = ( - output_glac_bin_dist - ) - output_ds_binned_stats['bin_surface_h_initial'].values[ - 0, : - ] = surface_h_initial - output_ds_binned_stats['bin_area_annual'].values[ - 0, :, : - ] = output_glac_bin_area_annual[:, :, n_iter] - output_ds_binned_stats['bin_mass_annual'].values[ - 0, :, : - ] = output_glac_bin_mass_annual[:, :, n_iter] - output_ds_binned_stats['bin_thick_annual'].values[ - 0, :, : - ] = output_glac_bin_icethickness_annual[:, :, n_iter] - output_ds_binned_stats['bin_massbalclim_annual'].values[ - 0, :, : - ] = output_glac_bin_massbalclim_annual[:, :, n_iter] - output_ds_binned_stats[ - 'bin_massbalclim_monthly' - ].values[0, :, :] = output_glac_bin_massbalclim_monthly[ + output_ds_binned_stats['bin_distance'].values[0, :] = output_glac_bin_dist + output_ds_binned_stats['bin_surface_h_initial'].values[0, :] = surface_h_initial + output_ds_binned_stats['bin_area_annual'].values[0, :, :] = output_glac_bin_area_annual[ :, :, n_iter ] + output_ds_binned_stats['bin_mass_annual'].values[0, :, :] = output_glac_bin_mass_annual[ + :, :, n_iter + ] + output_ds_binned_stats['bin_thick_annual'].values[0, :, :] = ( + output_glac_bin_icethickness_annual[:, :, n_iter] + ) + output_ds_binned_stats['bin_massbalclim_annual'].values[0, :, :] = ( + output_glac_bin_massbalclim_annual[:, :, n_iter] + ) + output_ds_binned_stats['bin_massbalclim_monthly'].values[0, :, :] = ( + output_glac_bin_massbalclim_monthly[:, :, n_iter] + ) if args.export_binned_components: - output_ds_binned_stats[ - 'bin_accumulation_monthly' - ].values[0, :, :] = output_glac_bin_acc_monthly[ - :, :, n_iter - ] - output_ds_binned_stats['bin_melt_monthly'].values[ - 0, :, : - ] = output_glac_bin_melt_monthly[:, :, n_iter] - output_ds_binned_stats[ - 'bin_refreeze_monthly' - ].values[ - 0, :, : - ] = output_glac_bin_refreeze_monthly[:, :, n_iter] + output_ds_binned_stats['bin_accumulation_monthly'].values[0, :, :] = ( + output_glac_bin_acc_monthly[:, :, n_iter] + ) + output_ds_binned_stats['bin_melt_monthly'].values[0, :, :] = ( + output_glac_bin_melt_monthly[:, :, n_iter] + ) + output_ds_binned_stats['bin_refreeze_monthly'].values[0, :, :] = ( + output_glac_bin_refreeze_monthly[:, :, n_iter] + ) # export binned stats for iteration output_binned.set_fn( - output_binned.get_fn().replace( - 'SETS', f'set{n_iter}' - ) + output_binned.get_fn().replace('SETS', f'set{n_iter}') + args.outputfn_sfix + 'binned.nc' ) @@ -2218,12 +1861,8 @@ def run(list_packed_vars): output_ds_binned_stats = output_binned.get_xr_ds() # populate dataset with stats from each variable of interest - output_ds_binned_stats[ - 'bin_distance' - ].values = output_glac_bin_dist[np.newaxis, :] - output_ds_binned_stats[ - 'bin_surface_h_initial' - ].values = surface_h_initial[np.newaxis, :] + output_ds_binned_stats['bin_distance'].values = output_glac_bin_dist[np.newaxis, :] + output_ds_binned_stats['bin_surface_h_initial'].values = surface_h_initial[np.newaxis, :] output_ds_binned_stats['bin_area_annual'].values = np.median( output_glac_bin_area_annual, axis=2 )[np.newaxis, :, :] @@ -2233,68 +1872,43 @@ def run(list_packed_vars): output_ds_binned_stats['bin_thick_annual'].values = np.median( output_glac_bin_icethickness_annual, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats[ - 'bin_massbalclim_annual' - ].values = np.median( + output_ds_binned_stats['bin_massbalclim_annual'].values = np.median( output_glac_bin_massbalclim_annual, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats[ - 'bin_massbalclim_monthly' - ].values = np.median( + output_ds_binned_stats['bin_massbalclim_monthly'].values = np.median( output_glac_bin_massbalclim_monthly, axis=2 )[np.newaxis, :, :] if args.export_binned_components: - output_ds_binned_stats[ - 'bin_accumulation_monthly' - ].values = np.median(output_glac_bin_acc_monthly, axis=2)[ - np.newaxis, :, : - ] - output_ds_binned_stats[ - 'bin_melt_monthly' - ].values = np.median(output_glac_bin_melt_monthly, axis=2)[ - np.newaxis, :, : - ] - output_ds_binned_stats[ - 'bin_refreeze_monthly' - ].values = np.median( + output_ds_binned_stats['bin_accumulation_monthly'].values = np.median( + output_glac_bin_acc_monthly, axis=2 + )[np.newaxis, :, :] + output_ds_binned_stats['bin_melt_monthly'].values = np.median( + output_glac_bin_melt_monthly, axis=2 + )[np.newaxis, :, :] + output_ds_binned_stats['bin_refreeze_monthly'].values = np.median( output_glac_bin_refreeze_monthly, axis=2 )[np.newaxis, :, :] if nsims > 1: - output_ds_binned_stats[ - 'bin_mass_annual_mad' - ].values = median_abs_deviation( + output_ds_binned_stats['bin_mass_annual_mad'].values = median_abs_deviation( output_glac_bin_mass_annual, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats[ - 'bin_thick_annual_mad' - ].values = median_abs_deviation( + output_ds_binned_stats['bin_thick_annual_mad'].values = median_abs_deviation( output_glac_bin_icethickness_annual, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats[ - 'bin_massbalclim_annual_mad' - ].values = median_abs_deviation( + output_ds_binned_stats['bin_massbalclim_annual_mad'].values = median_abs_deviation( output_glac_bin_massbalclim_annual, axis=2 )[np.newaxis, :, :] # export merged netcdf glacierwide stats output_binned.set_fn( - output_binned.get_fn().replace('SETS', f'{nsims}sets') - + args.outputfn_sfix - + 'binned.nc' + output_binned.get_fn().replace('SETS', f'{nsims}sets') + args.outputfn_sfix + 'binned.nc' ) output_binned.save_xr_ds() except Exception as err: # LOG FAILURE - fail_fp = ( - pygem_prms['root'] - + '/Output/simulations/failed/' - + reg_str - + '/' - + sim_climate_name - + '/' - ) - if sim_climate_name not in ['ERA-Interim', 'ERA5', 'COAWST']: + fail_fp = pygem_prms['root'] + '/Output/simulations/failed/' + reg_str + '/' + sim_climate_name + '/' + if sim_climate_name not in ['ERA5', 'COAWST']: fail_fp += sim_climate_scenario + '/' if not os.path.exists(fail_fp): os.makedirs(fail_fp, exist_ok=True) @@ -2351,9 +1965,7 @@ def main(): num_cores = 1 # Glacier number lists to pass for parallel processing - glac_no_lsts = modelsetup.split_list( - glac_no, n=num_cores, option_ordered=args.option_ordered - ) + glac_no_lsts = modelsetup.split_list(glac_no, n=num_cores, option_ordered=args.option_ordered) # Read GCM names from argument parser sim_climate_name = args.gcm_list_fn @@ -2394,14 +2006,10 @@ def main(): if realizations is not None: for realization in realizations: for count, glac_no_lst in enumerate(glac_no_lsts): - list_packed_vars.append( - [count, glac_no_lst, sim_climate_name, realization] - ) + list_packed_vars.append([count, glac_no_lst, sim_climate_name, realization]) else: for count, glac_no_lst in enumerate(glac_no_lsts): - list_packed_vars.append( - [count, glac_no_lst, sim_climate_name, realizations] - ) + list_packed_vars.append([count, glac_no_lst, sim_climate_name, realizations]) print('Processing with ' + str(num_cores) + ' cores...') # Parallel processing diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index 76f48554..9d3d03f3 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -1,10 +1,16 @@ import argparse import json import multiprocessing +import os +import warnings +from datetime import datetime from functools import partial +import matplotlib.pyplot as plt import numpy as np import pandas as pd +import xarray as xr +from scipy.stats import binned_statistic # pygem imports from pygem.setup.config import ConfigManager @@ -25,16 +31,124 @@ single_flowline_glacier_directory_with_calving, update_cfg, ) +from pygem.utils._funcs import interp1d_fill_gaps -def run(glacno_list, mb_model_params, debug=False, **kwargs): - # remove None kwargs - kwargs = {k: v for k, v in kwargs.items() if v is not None} +# get model monthly deltah +def get_elev_change_1d_hat(gdir): + # load flowline_diagnostics from spinup + f = gdir.get_filepath('fl_diagnostics', filesuffix='_dynamic_spinup_pygem_mb') + with xr.open_dataset(f, group='fl_0') as ds_spn: + ds_spn = ds_spn.load() + + # get binned surface area at spinup target year + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + area = np.where( + ds_spn.sel(time=gdir.rgi_date + 1).thickness_m.values > 0, + ds_spn.sel(time=gdir.rgi_date + 1).volume_m3.values / ds_spn.sel(time=gdir.rgi_date + 1).thickness_m.values, + 0, + ) + + thickness_m = ds_spn.thickness_m.values.T # glacier thickness [m ice], (nbins, nyears) + + # set any < 0 thickness to nan + thickness_m[thickness_m <= 0] = np.nan + + # climatic mass balance + dotb_monthly = np.repeat(ds_spn.climatic_mb_myr.values.T[:, 1:] / 12, 12, axis=-1) + + # convert to m ice + dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) + ### to get monthly thickness and mass we require monthly flux divergence ### + # we'll assume the flux divergence is constant througohut the year (is this a good assumption?) + # ie. take annual values and divide by 12 - use numpy repeat to repeat values across 12 months + flux_div_monthly_mmo = np.repeat(-ds_spn.flux_divergence_myr.values.T[:, 1:] / 12, 12, axis=-1) + # get monthly binned change in thickness + delta_h_monthly = dotb_monthly - flux_div_monthly_mmo # [m ice per month] + + # get binned monthly thickness = running thickness change + initial thickness + running_delta_h_monthly = np.cumsum(delta_h_monthly, axis=-1) + h_monthly = running_delta_h_monthly + thickness_m[:, 0][:, np.newaxis] + + # get surface height at the specified reference year + ref_surface_h = ds_spn.bed_h.values + ds_spn.thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values + + # aggregate model bin thicknesses as desired + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + h_monthly = np.column_stack( + [ + binned_statistic( + x=ref_surface_h, values=x, statistic=np.nanmean, bins=gdir.elev_change_1d['bin_edges'] + )[0] + for x in h_monthly.T + ] + ) + + # interpolate over any empty bins + h_monthly_ = np.column_stack([interp1d_fill_gaps(x.copy()) for x in h_monthly.T]) + + # difference desired time steps, return np.nan array for any missing inds + dh = np.column_stack( + [ + h_monthly_[:, j] - h_monthly_[:, i] + if (i is not None and j is not None and 0 <= i < h_monthly_.shape[1] and 0 <= j < h_monthly_.shape[1]) + else np.full(h_monthly_.shape[0], np.nan) + for i, j in gdir.elev_change_1d['model2obs_inds_map'] + ] + ) + return dh, ds_spn.dis_along_flowline.values, area + + +def loss_with_penalty(x, obs, mod, threshold=100, weight=1.0): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + # MAE where observations exist + mismatch = np.nanmean(np.abs(mod - obs)) + + # Penalty: positive modeled values below threshold + mask = x < threshold + mod_sub = mod[mask] + + # keep only positives + positives = np.clip(mod_sub, a_min=0, a_max=None) + + # add to loss (scales with mean positive magnitude) + penalty = weight * np.nanmean(positives) + return mismatch + penalty + + +# run spinup function +def run_spinup(gd, **kwargs): + out = workflow.execute_entity_task( + tasks.run_dynamic_spinup, + gd, + minimise_for='area', + output_filesuffix='_dynamic_spinup_pygem_mb', + store_fl_diagnostics=True, + store_model_geometry=True, + mb_model_historical=PyGEMMassBalance_wrapper(gd, fl_str='model_flowlines'), + ignore_errors=False, + **kwargs, + ) + return out + + +def run(glacno_list, mb_model_params, optimize=False, periods2try=[20], outdir=None, debug=False, ncores=1, **kwargs): + # remove any None kwargs + kwargs = {k: v for k, v in kwargs.items() if v is not None} main_glac_rgi = modelsetup.selectglaciersrgitable(glac_no=glacno_list) - # model dates - dt = modelsetup.datesmodelrun(startyear=1979, endyear=2019) - # load climate data + + # model dates - define model dates table that covers time span of interest + if optimize: + sy = 1940 + else: + sy = 2000 - kwargs.get('spinup_period', 20) + dt = modelsetup.datesmodelrun(startyear=sy, endyear=kwargs.get('ye', pygem_prms['climate']['ref_endyear'])) + + # Load climate data ref_clim = class_climate.GCM(name='ERA5') # Air temperature [degC] @@ -46,37 +160,26 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): ref_clim.prec_fn, ref_clim.prec_vn, main_glac_rgi, dt, verbose=debug ) # Elevation [m asl] - elev = ref_clim.importGCMfxnearestneighbor_xarray( - ref_clim.elev_fn, ref_clim.elev_vn, main_glac_rgi - ) + elev = ref_clim.importGCMfxnearestneighbor_xarray(ref_clim.elev_fn, ref_clim.elev_vn, main_glac_rgi) # Lapse rate [degC m-1] lr, _ = ref_clim.importGCMvarnearestneighbor_xarray( ref_clim.lr_fn, ref_clim.lr_vn, main_glac_rgi, dt, verbose=debug ) # load prior regionally averaged modelprms (from Rounce et al. 2023) - priors_df = pd.read_csv( - pygem_prms['root'] - + '/Output/calibration/' - + pygem_prms['calib']['priors_reg_fn'] - ) + priors_df = pd.read_csv(pygem_prms['root'] + '/Output/calibration/' + pygem_prms['calib']['priors_reg_fn']) # loop through gdirs and add `glacier_rgi_table`, `historical_climate`, `dates_table` and `modelprms` attributes to each glacier directory - for i, glaco in enumerate(glacno_list): + for i, glac_no in enumerate(glacno_list): try: glacier_rgi_table = main_glac_rgi.loc[main_glac_rgi.index.values[i], :] glacier_str = '{0:0.5f}'.format(glacier_rgi_table['RGIId_float']) # instantiate glacier directory - if ( - glacier_rgi_table['TermType'] not in [1, 5] - or not pygem_prms['setup']['include_frontalablation'] - ): + if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: gd = single_flowline_glacier_directory(glacier_str, reset=False) gd.is_tidewater = False else: - gd = single_flowline_glacier_directory_with_calving( - glacier_str, reset=False - ) + gd = single_flowline_glacier_directory_with_calving(glacier_str, reset=False) gd.is_tidewater = True # Select subsets of data @@ -92,6 +195,24 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): } gd.dates_table = dt + # ensure `ye` >= `target_year` + ty = kwargs.get('target_year', gd.rgi_date + 1) + ye = kwargs.get('ye', ty) + if ye < ty: + raise ValueError(f'Spinup end year (ye={ye}) must be greater than target year (target_yr={ty}):') + + if optimize: + # model ela - take median val + gd.ela = tasks.compute_ela(gd, years=[v for v in gd.dates_table.year.unique() if v <= 2019]).median() + # load elevation change data + if os.path.isfile(gd.get_filepath('elev_change_1d')): + gd.elev_change_1d = gd.read_json('elev_change_1d') + else: + gd.elev_change_1d = None + + ############################ + ####### model params ####### + ############################ if mb_model_params == 'regional_priors': # get modelprms from regional priors priors_idx = np.where( @@ -113,10 +234,7 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): # get modelprms from emulator mass balance calibration modelprms_fn = glacier_str + '-modelprms_dict.json' modelprms_fp = ( - pygem_prms['root'] - + '/Output/calibration/' - + glacier_str.split('.')[0].zfill(2) - + '/' + pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' ) + modelprms_fn with open(modelprms_fp, 'r') as f: modelprms_dict = json.load(f) @@ -130,32 +248,170 @@ def run(glacno_list, mb_model_params, debug=False, **kwargs): 'tsnow_threshold': modelprms_all['tsnow_threshold'][0], 'precgrad': modelprms_all['precgrad'][0], } + ############################ # update cfg.PARAMS update_cfg({'continue_on_error': True}, 'PARAMS') update_cfg({'store_model_geometry': True}, 'PARAMS') - # perform OGGM dynamic spinup - workflow.execute_entity_task( - tasks.run_dynamic_spinup, - gd, - # spinup_start_yr=spinup_start_yr, # When to start the spinup - minimise_for='area', # what target to match at the RGI date - # target_yr=target_yr, # The year at which we want to match area or volume. If None, gdir.rgi_date + 1 is used (the default) - # ye=ye, # When the simulation should stop - output_filesuffix='_dynamic_spinup_pygem_mb', - store_fl_diagnostics=True, - store_model_geometry=True, - # first_guess_t_spinup = , could be passed as input argument for each step in the sampler based on prior tbias, current default first guess is -2 - mb_model_historical=PyGEMMassBalance_wrapper( - gd, fl_str='model_flowlines' - ), - ignore_errors=False, - **kwargs, - ) + # optimize against binned dhdt data + spinup_period = None + if (optimize) and (gd.elev_change_1d is not None): + # get number of years between surveys to normalize by time + gd.elev_change_1d['nyrs'] = [] + for start, end in gd.elev_change_1d['dates']: + start_dt = datetime.strptime(start, '%Y-%m-%d') + end_dt = datetime.strptime(end, '%Y-%m-%d') + gd.elev_change_1d['nyrs'].append((end_dt - start_dt).days / 365.25) + gd.elev_change_1d['dhdt'] = np.column_stack(gd.elev_change_1d['dh']) / gd.elev_change_1d['nyrs'] + + results = {} # instantiate output dictionary + fig, ax = plt.subplots(1) # instantiate figure + + # objective function to evaluate + def _objective(**kwargs): + fls = run_spinup(gd, **kwargs) + + # get true spinup period (note, if initial fails, oggm tries period/2) + spinup_period_ = gd.rgi_date + 1 - fls[0].y0 + + # create lookup dict (timestamp → index) + dtable = modelsetup.datesmodelrun(startyear=fls[0].y0, endyear=kwargs['ye']) + date_to_index = {d: i for i, d in enumerate(dtable['date'])} + gd.elev_change_1d['model2obs_inds_map'] = [ + ( + date_to_index.get(pd.to_datetime(start)), + date_to_index.get(pd.to_datetime(end)), + ) + for start, end in gd.elev_change_1d['dates'] + ] + + dh_hat, dist, bin_area = get_elev_change_1d_hat(gd) + dhdt_hat = dh_hat / gd.elev_change_1d['nyrs'] + + # plot binned surface area + ax.plot(dist, bin_area, label=f'{spinup_period} years: {round(1e-6 * np.sum(bin_area), 1)} km$^2$') + + # penalize positive values below specified elevation threshold + loss = loss_with_penalty( + gd.elev_change_1d['bin_centers'], gd.elev_change_1d['dhdt'], dhdt_hat, gd.ela + ) + + return spinup_period_, loss, dhdt_hat + + # evaluate candidate spinup periods + for p in periods2try: + kwargs['spinup_period'] = p + p_, mismatch, model = _objective(**kwargs) + results[p_] = (mismatch, model) + + # find best + best_period = min(results, key=lambda k: results[k][0]) + best_value, best_model = results[best_period] + # update kwarg + kwargs['spinup_period'] = best_period + + if debug: + print('All results:', {k: v[0] for k, v in results.items()}) + print(f'Best spinup_period = {best_period}, mismatch = {best_value}') + + # find worst + worst_period = max(results, key=lambda k: results[k][0]) + worst_value, worst_model = results[worst_period] + + ############################ + ### diagnostics plotting ### + ############################ + # binned area + ax.legend() + ax.set_title(gd.rgi_id) + ax.set_xlabel('distance along flowline (m)') + ax.set_ylabel('surface area (m$^2$)') + ax.set_xlim([0, ax.get_xlim()[1]]) + ax.set_ylim([0, ax.get_ylim()[1]]) + fig.tight_layout() + if debug and ncores == 1: + plt.show() + if outdir: + fig.savefig(f'{outdir}/{glac_no}-spinup_binned_area.png', dpi=300) + plt.close() + + # 1d elevation change + labels = [ + (f'{start[:-2].replace("-", "")}:{end[:-3].replace("-", "")}') + for start, end in gd.elev_change_1d['dates'] + ] + fig, ax = plt.subplots(figsize=(8, 5)) + + for t in range(gd.elev_change_1d['dhdt'].shape[1]): + # plot Obs first, grab the color + (line,) = ax.plot( + gd.elev_change_1d['bin_centers'], + gd.elev_change_1d['dhdt'][:, t], + linestyle='-', + marker='.', + label=labels[t], + ) + color = line.get_color() + + # plot Best model with same color + ax.plot( + gd.elev_change_1d['bin_centers'], + best_model[:, t], + linestyle='--', + marker='.', + color=color, + ) + + # plot Worst model with same color + ax.plot( + gd.elev_change_1d['bin_centers'], + worst_model[:, t], + linestyle=':', + marker='.', + color=color, + ) + ax.axvline(gd.ela, c='grey', ls=':') + ax.axhline(0, c='grey', ls='-') + ax.plot([], [], 'k--', label=r'$\hat{best}$') + ax.plot([], [], 'k:', label=r'$\hat{worst}$') + ax.set_xlabel('elevation (m)') + ax.set_ylabel(r'elevation change (m yr$^{-1}$)') + ax.set_title( + f'{glac_no}\nBest={best_period} (mismatch={best_value:.3f}), ' + f'Worst={worst_period} (mismatch={worst_value:.3f})' + ) + ax.legend(handlelength=1, borderaxespad=0, fancybox=False) + # plot area + if 'bin_area' in gd.elev_change_1d: + area = np.array(gd.elev_change_1d['bin_area']) + area_mask = area > 0 + ax2 = ax.twinx() # shares x-axis + ax2.fill_between( + np.array(gd.elev_change_1d['bin_centers'])[area_mask], + 0, + area[area_mask], + color='gray', + alpha=0.1, + ) + ax2.set_ylim([0, ax2.get_ylim()[1]]) + ax2.set_ylabel(r'area (m $^{2}$)', color='gray') + ax2.tick_params(axis='y', colors='gray') + ax2.spines['right'].set_color('gray') + ax2.yaxis.label.set_color('gray') + fig.tight_layout() + if debug and ncores == 1: + plt.show() + if outdir: + fig.savefig(f'{outdir}/{glac_no}-spinup_optimization.png', dpi=300) + plt.close() + ############################ + + # update spinup_period if optimized or specified as CLI argument, else remove kwarg and use OGGM default + run_spinup(gd, **kwargs) except Exception as e: - print(f'Error processing glacier {glaco}: {e}') + print(f'Error processing glacier {glac_no}: {e}') # continue to next glacier continue @@ -195,14 +451,8 @@ def main(): help='Filepath containing list of rgi_glac_number, helpful for running batches on spc', ), ) - parser.add_argument( - '-spinup_period', - type=int, - default=None, - help='Fixed spinup period (years). If not provided, OGGM default is used.', - ) parser.add_argument('-target_yr', type=int, default=None) - parser.add_argument('-ye', type=int, default=2020) + parser.add_argument('-ye', type=int, default=None) parser.add_argument( '-ncores', action='store', @@ -213,12 +463,46 @@ def main(): parser.add_argument( '-mb_model_params', type=str, - default='regional_priors', + default='emulator', choices=['regional_priors', 'emulator'], help='Which mass balance model parameters to use ("regional_priors" or "emulator")', ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '-spinup_period', + type=int, + default=None, + help='Fixed spinup period (years). If not provided, OGGM default is used.', + ) + group.add_argument( + '-optimize', + action='store_true', + help=( + 'Optimize the spinup_period by minimizing against elevation change data. ' + 'This goes through spinup periods of [20,30,40,50,60] years and finds the one ' + 'that gives the best fit to any available 1d elevation change data.' + ), + ) + parser.add_argument( + '-periods2try', + type=int, + nargs='+', + default=[20, 30, 40, 50, 60], + help=( + 'Optional list of spinup periods (years) to test if -optimize is used. ' + 'Ignored otherwise. Example: -periods2try 20 30 40 50 60' + ), + ) + parser.add_argument( + '-outdir', type=str, default=None, help='Directory to store any ouputs (diagnostic figures, etc.)' + ) + parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') args = parser.parse_args() + # --- Validation logic --- + if args.optimize and args.ye is None: + parser.error("When '-optimize' is True, must specify `ye` (spinup end year).") + # RGI glacier number glac_no = None if args.rgi_glac_number: @@ -241,9 +525,7 @@ def main(): glac_no = list(main_glac_rgi_all['rgino_str'].values) if glac_no is None: - raise ValueError( - 'Need to specify either -rgi_glac_number or -rgi_glac_number_fn' - ) + raise ValueError('Need to specify either -rgi_glac_number or -rgi_glac_number_fn') # number of cores for parallel processing if args.ncores > 1: @@ -257,10 +539,17 @@ def main(): # set up partial function with debug argument run_partial = partial( run, + optimize=args.optimize, + periods2try=args.periods2try, + outdir=args.outdir, + debug=args.debug, + ncores=ncores, mb_model_params=args.mb_model_params, target_yr=args.target_yr, spinup_period=args.spinup_period, + ye=args.ye, ) + # parallel processing print(f'Processing with {ncores} cores... \n{glac_no_lsts}') with multiprocessing.Pool(ncores) as p: diff --git a/pygem/class_climate.py b/pygem/class_climate.py index 7d964067..5cb81865 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -45,9 +45,7 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): """ if pygem_prms['rgi']['rgi_lon_colname'] not in ['CenLon_360']: - assert 1 == 0, ( - 'Longitude does not use 360 degrees. Check how negative values are handled!' - ) + assert 1 == 0, 'Longitude does not use 360 degrees. Check how negative values are handled!' # Source of climate data self.name = name @@ -90,14 +88,7 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + realization + '.cam.h0.1980-2100.nc' ) - self.elev_fn = ( - self.elev_vn - + '_fx_' - + sim_climate_scenario - + '_' - + name - + '.cam.h0.nc' - ) + self.elev_fn = self.elev_vn + '_fx_' + sim_climate_scenario + '_' + name + '.cam.h0.nc' # Variable filepaths self.var_fp = ( pygem_prms['root'] @@ -149,9 +140,7 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): + realization + 'i1p1f1_gr3_1980-2100.nc' ) - self.elev_fn = ( - self.elev_vn + '_fx_' + sim_climate_scenario + '_' + name + '.nc' - ) + self.elev_fn = self.elev_vn + '_fx_' + sim_climate_scenario + '_' + name + '.nc' # Variable filepaths self.var_fp = ( pygem_prms['root'] @@ -194,14 +183,8 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.lr_fn = pygem_prms['climate']['paths']['era5_lr_fn'] # Variable filepaths if pygem_prms['climate']['paths']['era5_relpath']: - self.var_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['era5_relpath'] - ) - self.fx_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['era5_relpath'] - ) + self.var_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] + self.fx_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['era5_relpath'] else: self.var_fp = '' self.fx_fp = '' @@ -211,35 +194,6 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] - elif self.name == 'ERA-Interim': - # Variable names - self.temp_vn = 't2m' - self.prec_vn = 'tp' - self.elev_vn = 'z' - self.lat_vn = 'latitude' - self.lon_vn = 'longitude' - self.time_vn = 'time' - self.lr_vn = 'lapserate' - # Variable filenames - self.temp_fn = pygem_prms['climate']['paths']['eraint_temp_fn'] - self.prec_fn = pygem_prms['climate']['paths']['eraint_prec_fn'] - self.elev_fn = pygem_prms['climate']['paths']['eraint_elev_fn'] - self.lr_fn = pygem_prms['climate']['paths']['eraint_lr_fn'] - # Variable filepaths - self.var_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['eraint_relpath'] - ) - self.fx_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['eraint_relpath'] - ) - - # Extra information - self.timestep = pygem_prms['time']['timestep'] - self.rgi_lat_colname = pygem_prms['rgi']['rgi_lat_colname'] - self.rgi_lon_colname = pygem_prms['rgi']['rgi_lon_colname'] - # Standardized CMIP5 format (GCM/RCP) elif 'rcp' in sim_climate_scenario: # Variable names @@ -250,30 +204,9 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.lon_vn = 'lon' self.time_vn = 'time' # Variable filenames - self.temp_fn = ( - self.temp_vn - + '_mon_' - + name - + '_' - + sim_climate_scenario - + '_r1i1p1_native.nc' - ) - self.prec_fn = ( - self.prec_vn - + '_mon_' - + name - + '_' - + sim_climate_scenario - + '_r1i1p1_native.nc' - ) - self.elev_fn = ( - self.elev_vn - + '_fx_' - + name - + '_' - + sim_climate_scenario - + '_r0i0p0.nc' - ) + self.temp_fn = self.temp_vn + '_mon_' + name + '_' + sim_climate_scenario + '_r1i1p1_native.nc' + self.prec_fn = self.prec_vn + '_mon_' + name + '_' + sim_climate_scenario + '_r1i1p1_native.nc' + self.elev_fn = self.elev_vn + '_fx_' + name + '_' + sim_climate_scenario + '_r0i0p0.nc' # Variable filepaths self.var_fp = ( pygem_prms['root'] @@ -290,21 +223,11 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): if not os.path.exists(self.var_fp) and os.path.exists( pygem_prms['climate']['paths']['cmip5_relpath'] + name + '/' ): - self.var_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['cmip5_relpath'] - + name - + '/' - ) + self.var_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['cmip5_relpath'] + name + '/' if not os.path.exists(self.fx_fp) and os.path.exists( pygem_prms['climate']['paths']['cmip5_relpath'] + name + '/' ): - self.fx_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['cmip5_relpath'] - + name - + '/' - ) + self.fx_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['cmip5_relpath'] + name + '/' # Extra information # self.timestep = pygem_prms['time']['timestep'] self.timestep = 'monthly' # future scenario is always monthly timestep @@ -322,36 +245,12 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): self.lon_vn = 'lon' self.time_vn = 'time' # Variable filenames - self.temp_fn = ( - name - + '_' - + sim_climate_scenario - + '_r1i1p1f1_' - + self.temp_vn - + '.nc' - ) - self.prec_fn = ( - name - + '_' - + sim_climate_scenario - + '_r1i1p1f1_' - + self.prec_vn - + '.nc' - ) + self.temp_fn = name + '_' + sim_climate_scenario + '_r1i1p1f1_' + self.temp_vn + '.nc' + self.prec_fn = name + '_' + sim_climate_scenario + '_r1i1p1f1_' + self.prec_vn + '.nc' self.elev_fn = name + '_' + self.elev_vn + '.nc' # Variable filepaths - self.var_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['cmip6_relpath'] - + name - + '/' - ) - self.fx_fp = ( - pygem_prms['root'] - + pygem_prms['climate']['paths']['cmip6_relpath'] - + name - + '/' - ) + self.var_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['cmip6_relpath'] + name + '/' + self.fx_fp = pygem_prms['root'] + pygem_prms['climate']['paths']['cmip6_relpath'] + name + '/' # Extra information # self.timestep = pygem_prms['time']['timestep'] self.timestep = 'monthly' # future scenario is always monthly timestep @@ -363,7 +262,7 @@ def importGCMfxnearestneighbor_xarray(self, filename, vn, main_glac_rgi): """ Import time invariant (constant) variables and extract nearest neighbor. - Note: cmip5 data used surface height, while ERA-Interim data is geopotential + Note: cmip5 data used surface height, while ERA5 data is geopotential Parameters ---------- @@ -390,34 +289,20 @@ def importGCMfxnearestneighbor_xarray(self, filename, vn, main_glac_rgi): if self.name == 'COAWST': for glac in range(main_glac_rgi.shape[0]): latlon_dist = ( - ( - data[self.lat_vn].values - - main_glac_rgi[self.rgi_lat_colname].values[glac] - ) - ** 2 - + ( - data[self.lon_vn].values - - main_glac_rgi[self.rgi_lon_colname].values[glac] - ) - ** 2 + (data[self.lat_vn].values - main_glac_rgi[self.rgi_lat_colname].values[glac]) ** 2 + + (data[self.lon_vn].values - main_glac_rgi[self.rgi_lon_colname].values[glac]) ** 2 ) ** 0.5 - latlon_nearidx = [ - x[0] for x in np.where(latlon_dist == latlon_dist.min()) - ] + latlon_nearidx = [x[0] for x in np.where(latlon_dist == latlon_dist.min())] lat_nearidx = latlon_nearidx[0] lon_nearidx = latlon_nearidx[1] - glac_variable[glac] = data[vn][ - latlon_nearidx[0], latlon_nearidx[1] - ].values + glac_variable[glac] = data[vn][latlon_nearidx[0], latlon_nearidx[1]].values else: # argmin() finds the minimum distance between the glacier lat/lon and the GCM pixel lat_nearidx = np.abs( - main_glac_rgi[self.rgi_lat_colname].values[:, np.newaxis] - - data.variables[self.lat_vn][:].values + main_glac_rgi[self.rgi_lat_colname].values[:, np.newaxis] - data.variables[self.lat_vn][:].values ).argmin(axis=1) lon_nearidx = np.abs( - main_glac_rgi[self.rgi_lon_colname].values[:, np.newaxis] - - data.variables[self.lon_vn][:].values + main_glac_rgi[self.rgi_lon_colname].values[:, np.newaxis] - data.variables[self.lon_vn][:].values ).argmin(axis=1) latlon_nearidx = list(zip(lat_nearidx, lon_nearidx)) @@ -426,9 +311,7 @@ def importGCMfxnearestneighbor_xarray(self, filename, vn, main_glac_rgi): glac_variable_dict = {} for latlon in latlon_nearidx_unique: try: - glac_variable_dict[latlon] = data[vn][ - time_idx, latlon[0], latlon[1] - ].values + glac_variable_dict[latlon] = data[vn][time_idx, latlon[0], latlon[1]].values except: glac_variable_dict[latlon] = data[vn][latlon[0], latlon[1]].values @@ -489,9 +372,7 @@ def importGCMvarnearestneighbor_xarray( # Import netcdf file if self.timestep == 'monthly': if not os.path.exists(self.var_fp + filename): - if os.path.exists( - self.var_fp + filename.replace('r1i1p1f1', 'r4i1p1f1') - ): + if os.path.exists(self.var_fp + filename.replace('r1i1p1f1', 'r4i1p1f1')): filename = filename.replace('r1i1p1f1', 'r4i1p1f1') if os.path.exists(self.var_fp + filename.replace('_native', '')): filename = filename.replace('_native', '') @@ -509,17 +390,13 @@ def importGCMvarnearestneighbor_xarray( if 'YYYY' in filename: datasets = [] for yr in range(year_start, year_end + 1): - data_yr = xr.open_dataset( - self.var_fp + filename.replace('YYYY', str(yr)) - ) + data_yr = xr.open_dataset(self.var_fp + filename.replace('YYYY', str(yr))) if 'valid_time' in data_yr.coords or 'valid_time' in data_yr.dims: data_yr = data_yr.rename({'valid_time': self.time_vn}) # convert longitude from -180—180 to 0—360 if data_yr.longitude.min() < 0: - data_yr = data_yr.assign_coords( - longitude=(data_yr.longitude % 360) - ) + data_yr = data_yr.assign_coords(longitude=(data_yr.longitude % 360)) # subset for desired lats and lons data_yr = data_yr.sel( @@ -535,9 +412,7 @@ def importGCMvarnearestneighbor_xarray( else: data = xr.open_dataset(self.var_fp + filename) - data = data.sel( - latitude=slice(max_lat, min_lat), longitude=slice(min_lon, max_lon) - ) + data = data.sel(latitude=slice(max_lat, min_lat), longitude=slice(min_lon, max_lon)) # mask out leap days if pygem_prms['time']['option_leapyear'] == 0 and not upscale_var_timestep: @@ -554,9 +429,7 @@ def importGCMvarnearestneighbor_xarray( # create empty DataArray for daily data daily_times = dates_table['date'].values daily_data = xr.DataArray( - np.zeros( - (len(daily_times), len(data.latitude), len(data.longitude)) - ), + np.zeros((len(daily_times), len(data.latitude), len(data.longitude))), dims=(self.time_vn, 'latitude', 'longitude'), coords={ self.time_vn: daily_times, @@ -569,10 +442,7 @@ def importGCMvarnearestneighbor_xarray( # loop through months and fill daily slots for i, t in enumerate(time_monthly): # find all days in this month - idx = np.where( - (dates_table['year'] == t.year) - & (dates_table['month'] == t.month) - )[0] + idx = np.where((dates_table['year'] == t.year) & (dates_table['month'] == t.month))[0] # assign monthly values to these daily indices daily_data[idx, :, :] = var_monthly.isel(time=i).values @@ -602,9 +472,7 @@ def importGCMvarnearestneighbor_xarray( end_idx = ( np.where( pd.Series(data[self.time_vn]).apply(lambda x: x.strftime('%Y-%m')) - == dates_table['date'].apply(lambda x: x.strftime('%Y-%m'))[ - dates_table.shape[0] - 1 - ] + == dates_table['date'].apply(lambda x: x.strftime('%Y-%m'))[dates_table.shape[0] - 1] ) )[0][0] # np.where finds the index position where to values are equal @@ -623,22 +491,14 @@ def importGCMvarnearestneighbor_xarray( elif self.timestep == 'daily': start_idx = ( np.where( - pd.Series(data[self.time_vn]).apply( - lambda x: x.strftime('%Y-%m-%d') - ) - == dates_table['date'] - .apply(lambda x: x.strftime('%Y-%m-%d')) - .iloc[0] + pd.Series(data[self.time_vn]).apply(lambda x: x.strftime('%Y-%m-%d')) + == dates_table['date'].apply(lambda x: x.strftime('%Y-%m-%d')).iloc[0] ) )[0][0] end_idx = ( np.where( - pd.Series(data[self.time_vn]).apply( - lambda x: x.strftime('%Y-%m-%d') - ) - == dates_table['date'] - .apply(lambda x: x.strftime('%Y-%m-%d')) - .iloc[-1] + pd.Series(data[self.time_vn]).apply(lambda x: x.strftime('%Y-%m-%d')) + == dates_table['date'].apply(lambda x: x.strftime('%Y-%m-%d')).iloc[-1] ) )[0][0] @@ -648,20 +508,10 @@ def importGCMvarnearestneighbor_xarray( if self.name == 'COAWST': for glac in range(main_glac_rgi.shape[0]): latlon_dist = ( - ( - data[self.lat_vn].values - - main_glac_rgi[self.rgi_lat_colname].values[glac] - ) - ** 2 - + ( - data[self.lon_vn].values - - main_glac_rgi[self.rgi_lon_colname].values[glac] - ) - ** 2 + (data[self.lat_vn].values - main_glac_rgi[self.rgi_lat_colname].values[glac]) ** 2 + + (data[self.lon_vn].values - main_glac_rgi[self.rgi_lon_colname].values[glac]) ** 2 ) ** 0.5 - latlon_nearidx = [ - x[0] for x in np.where(latlon_dist == latlon_dist.min()) - ] + latlon_nearidx = [x[0] for x in np.where(latlon_dist == latlon_dist.min())] lat_nearidx = latlon_nearidx[0] lon_nearidx = latlon_nearidx[1] glac_variable_series[glac, :] = data[vn][ @@ -671,12 +521,10 @@ def importGCMvarnearestneighbor_xarray( # argmin() finds the minimum distance between the glacier lat/lon and the GCM pixel; .values is used to # extract the position's value as opposed to having an array lat_nearidx = np.abs( - main_glac_rgi[self.rgi_lat_colname].values[:, np.newaxis] - - data.variables[self.lat_vn][:].values + main_glac_rgi[self.rgi_lat_colname].values[:, np.newaxis] - data.variables[self.lat_vn][:].values ).argmin(axis=1) lon_nearidx = np.abs( - main_glac_rgi[self.rgi_lon_colname].values[:, np.newaxis] - - data.variables[self.lon_vn][:].values + main_glac_rgi[self.rgi_lon_colname].values[:, np.newaxis] - data.variables[self.lon_vn][:].values ).argmin(axis=1) # Find unique latitude/longitudes latlon_nearidx = list(zip(lat_nearidx, lon_nearidx)) @@ -691,18 +539,12 @@ def importGCMvarnearestneighbor_xarray( start_idx : end_idx + 1, expver_idx, latlon[0], latlon[1] ].values else: - glac_variable_dict[latlon] = data[vn][ - start_idx : end_idx + 1, latlon[0], latlon[1] - ].values + glac_variable_dict[latlon] = data[vn][start_idx : end_idx + 1, latlon[0], latlon[1]].values # Check all glacier use appropriate climate data for i, latlon in enumerate(latlon_nearidx): - rgi_id = main_glac_rgi[ - pygem_prms['rgi']['rgi_glacno_float_colname'] - ].values[i] - if (len(data[vn][self.lat_vn].values) == 1) or ( - len(data[vn][self.lon_vn].values) == 1 - ): + rgi_id = main_glac_rgi[pygem_prms['rgi']['rgi_glacno_float_colname']].values[i] + if (len(data[vn][self.lat_vn].values) == 1) or (len(data[vn][self.lon_vn].values) == 1): if verbose: warnings.warn( f'{vn} data has only one latitude or longitude value; check that the correct data is being used', @@ -713,12 +555,10 @@ def importGCMvarnearestneighbor_xarray( lat_res = abs(np.diff(data[vn][self.lat_vn].values)[0]) lon_res = abs(np.diff(data[vn][self.lon_vn].values)[0]) lat_dd = abs( - main_glac_rgi[self.rgi_lat_colname].values[i] - - data[vn][self.lat_vn].values[latlon[0]] + main_glac_rgi[self.rgi_lat_colname].values[i] - data[vn][self.lat_vn].values[latlon[0]] ) lon_dd = abs( - main_glac_rgi[self.rgi_lon_colname].values[i] - - data[vn][self.lon_vn].values[latlon[1]] + main_glac_rgi[self.rgi_lon_colname].values[i] - data[vn][self.lon_vn].values[latlon[1]] ) assert lat_dd <= lat_res and lon_dd <= lon_res, ( @@ -727,9 +567,7 @@ def importGCMvarnearestneighbor_xarray( ) # Convert to series - glac_variable_series = np.array( - [glac_variable_dict[x] for x in latlon_nearidx] - ) + glac_variable_series = np.array([glac_variable_dict[x] for x in latlon_nearidx]) # Perform corrections to the data if necessary # Surface air temperature corrections @@ -741,9 +579,7 @@ def importGCMvarnearestneighbor_xarray( print('Check units of air temperature from GCM is degrees C.') elif vn in ['t2m_std']: if 'units' in data[vn].attrs and data[vn].attrs['units'] not in ['C', 'K']: - print( - 'Check units of air temperature standard deviation from GCM is degrees C or K' - ) + print('Check units of air temperature standard deviation from GCM is degrees C or K') # Precipitation corrections # If the variable is precipitation elif vn in ['pr', 'tp', 'TOTPRECIP']: @@ -764,10 +600,7 @@ def importGCMvarnearestneighbor_xarray( if self.timestep == 'monthly' and self.name != 'COAWST': # Convert from meters per day to meters per month (COAWST data already 'monthly accumulated precipitation') if 'daysinmonth' in dates_table.columns: - glac_variable_series = ( - glac_variable_series - * dates_table['daysinmonth'].values[np.newaxis, :] - ) + glac_variable_series = glac_variable_series * dates_table['daysinmonth'].values[np.newaxis, :] elif vn != self.lr_vn: print('Check units of air temperature or precipitation') diff --git a/pygem/gcmbiasadj.py b/pygem/gcmbiasadj.py index fb935fab..2dc47b11 100755 --- a/pygem/gcmbiasadj.py +++ b/pygem/gcmbiasadj.py @@ -43,28 +43,14 @@ def monthly_avg_2darray(x): """ Monthly average for a given 2d dataset where columns are monthly timeseries """ - return ( - x.reshape(-1, 12) - .transpose() - .reshape(-1, int(x.shape[1] / 12)) - .mean(1) - .reshape(12, -1) - .transpose() - ) + return x.reshape(-1, 12).transpose().reshape(-1, int(x.shape[1] / 12)).mean(1).reshape(12, -1).transpose() def monthly_std_2darray(x): """ Monthly standard deviation for a given 2d dataset where columns are monthly timeseries """ - return ( - x.reshape(-1, 12) - .transpose() - .reshape(-1, int(x.shape[1] / 12)) - .std(1) - .reshape(12, -1) - .transpose() - ) + return x.reshape(-1, 12).transpose().reshape(-1, int(x.shape[1] / 12)).std(1).reshape(12, -1).transpose() def temp_biasadj_HH2015( @@ -103,12 +89,8 @@ def temp_biasadj_HH2015( new gcm elevation is the elevation of the reference climate dataset """ # GCM subset to agree with reference time period to calculate bias corrections - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] - gcm_subset_idx_end = np.where( - dates_table.date.values == dates_table_ref.date.values[-1] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] + gcm_subset_idx_end = np.where(dates_table.date.values == dates_table_ref.date.values[-1])[0][0] gcm_temp_subset = gcm_temp[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] # Roll months so they are aligned with simulation months @@ -118,14 +100,10 @@ def temp_biasadj_HH2015( # Mean monthly temperature ref_temp_monthly_avg = np.roll(monthly_avg_2darray(ref_temp), roll_amt, axis=1) - gcm_temp_monthly_avg = np.roll( - monthly_avg_2darray(gcm_temp_subset), roll_amt, axis=1 - ) + gcm_temp_monthly_avg = np.roll(monthly_avg_2darray(gcm_temp_subset), roll_amt, axis=1) # Standard deviation monthly temperature ref_temp_monthly_std = np.roll(monthly_std_2darray(ref_temp), roll_amt, axis=1) - gcm_temp_monthly_std = np.roll( - monthly_std_2darray(gcm_temp_subset), roll_amt, axis=1 - ) + gcm_temp_monthly_std = np.roll(monthly_std_2darray(gcm_temp_subset), roll_amt, axis=1) # Monthly bias adjustment (additive) gcm_temp_monthly_adj = ref_temp_monthly_avg - gcm_temp_monthly_avg @@ -162,38 +140,23 @@ def temp_biasadj_HH2015( t_m_Navg_subset = uniform_filter(t_m_subset, size=(1, N)) t_m_Navg[:, month::12] = t_m_Navg_subset - gcm_temp_biasadj = t_m_Navg + (t_mt - t_m_Navg) * np.tile( - variability_monthly_std, int(bc_temp.shape[1] / 12) - ) + gcm_temp_biasadj = t_m_Navg + (t_mt - t_m_Navg) * np.tile(variability_monthly_std, int(bc_temp.shape[1] / 12)) # Update elevation gcm_elev_biasadj = ref_elev # Assert that mean temperatures for all the glaciers must be more-or-less equal - gcm_temp_biasadj_subset = gcm_temp_biasadj[ - :, gcm_subset_idx_start : gcm_subset_idx_end + 1 - ] + gcm_temp_biasadj_subset = gcm_temp_biasadj[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] if sim_startyear == ref_startyear: if debug: - print( - (np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1)) - ) - assert ( - np.max( - np.abs( - np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1) - ) - ) - < 1 - ), ( + print((np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1))) + assert np.max(np.abs(np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1))) < 1, ( 'Error with gcm temperature bias adjustment: mean ref and gcm temps differ by more than 1 degree' ) else: if debug: - print( - (np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1)) - ) + print((np.mean(gcm_temp_biasadj_subset, axis=1) - np.mean(ref_temp, axis=1))) return gcm_temp_biasadj, gcm_elev_biasadj @@ -229,12 +192,8 @@ def prec_biasadj_HH2015( GCM precipitation bias corrected to the reference climate dataset according to Huss and Hock (2015) """ # GCM subset to agree with reference time period to calculate bias corrections - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] - gcm_subset_idx_end = np.where( - dates_table.date.values == dates_table_ref.date.values[-1] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] + gcm_subset_idx_end = np.where(dates_table.date.values == dates_table_ref.date.values[-1])[0][0] gcm_prec_subset = gcm_prec[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] # Roll months so they are aligned with simulation months @@ -243,9 +202,7 @@ def prec_biasadj_HH2015( # PRECIPITATION BIAS CORRECTIONS # Monthly mean precipitation ref_prec_monthly_avg = np.roll(monthly_avg_2darray(ref_prec), roll_amt, axis=1) - gcm_prec_monthly_avg = np.roll( - monthly_avg_2darray(gcm_prec_subset), roll_amt, axis=1 - ) + gcm_prec_monthly_avg = np.roll(monthly_avg_2darray(gcm_prec_subset), roll_amt, axis=1) bias_adj_prec_monthly = ref_prec_monthly_avg / gcm_prec_monthly_avg # if/else statement for whether or not the full GCM period is the same as the simulation period @@ -262,24 +219,16 @@ def prec_biasadj_HH2015( bc_prec = gcm_prec[:, sim_idx_start:] # Bias adjusted precipitation accounting for differences in monthly mean - gcm_prec_biasadj = bc_prec * np.tile( - bias_adj_prec_monthly, int(bc_prec.shape[1] / 12) - ) + gcm_prec_biasadj = bc_prec * np.tile(bias_adj_prec_monthly, int(bc_prec.shape[1] / 12)) # Update elevation gcm_elev_biasadj = ref_elev # Assertion that bias adjustment does not drastically modify the precipitation and are reasonable - gcm_prec_biasadj_subset = gcm_prec_biasadj[ - :, gcm_subset_idx_start : gcm_subset_idx_end + 1 - ] + gcm_prec_biasadj_subset = gcm_prec_biasadj[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] gcm_prec_biasadj_frac = gcm_prec_biasadj_subset.sum(axis=1) / ref_prec.sum(axis=1) - assert gcm_prec_biasadj.max() <= 10, ( - 'gcm_prec_adj (precipitation bias adjustment) too high, needs to be modified' - ) - assert gcm_prec_biasadj.min() >= 0, ( - 'gcm_prec_adj is producing a negative precipitation value' - ) + assert gcm_prec_biasadj.max() <= 10, 'gcm_prec_adj (precipitation bias adjustment) too high, needs to be modified' + assert gcm_prec_biasadj.min() >= 0, 'gcm_prec_adj is producing a negative precipitation value' return gcm_prec_biasadj, gcm_elev_biasadj, gcm_prec_biasadj_frac @@ -315,12 +264,8 @@ def prec_biasadj_opt1( new gcm elevation is the elevation of the reference climate dataset """ # GCM subset to agree with reference time period to calculate bias corrections - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] - gcm_subset_idx_end = np.where( - dates_table.date.values == dates_table_ref.date.values[-1] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] + gcm_subset_idx_end = np.where(dates_table.date.values == dates_table_ref.date.values[-1])[0][0] gcm_prec_subset = gcm_prec[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] # Roll months so they are aligned with simulation months @@ -329,9 +274,7 @@ def prec_biasadj_opt1( # PRECIPITATION BIAS CORRECTIONS # Monthly mean precipitation ref_prec_monthly_avg = np.roll(monthly_avg_2darray(ref_prec), roll_amt, axis=1) - gcm_prec_monthly_avg = np.roll( - monthly_avg_2darray(gcm_prec_subset), roll_amt, axis=1 - ) + gcm_prec_monthly_avg = np.roll(monthly_avg_2darray(gcm_prec_subset), roll_amt, axis=1) bias_adj_prec_monthly = ref_prec_monthly_avg / gcm_prec_monthly_avg # if/else statement for whether or not the full GCM period is the same as the simulation period @@ -348,28 +291,19 @@ def prec_biasadj_opt1( bc_prec = gcm_prec[:, sim_idx_start:] # Bias adjusted precipitation accounting for differences in monthly mean - gcm_prec_biasadj_raw = bc_prec * np.tile( - bias_adj_prec_monthly, int(bc_prec.shape[1] / 12) - ) + gcm_prec_biasadj_raw = bc_prec * np.tile(bias_adj_prec_monthly, int(bc_prec.shape[1] / 12)) # Adjust variance based on zscore and reference standard deviation ref_prec_monthly_std = np.roll(monthly_std_2darray(ref_prec), roll_amt, axis=1) - gcm_prec_biasadj_raw_monthly_avg = monthly_avg_2darray( - gcm_prec_biasadj_raw[:, 0 : ref_prec.shape[1]] - ) - gcm_prec_biasadj_raw_monthly_std = monthly_std_2darray( - gcm_prec_biasadj_raw[:, 0 : ref_prec.shape[1]] - ) + gcm_prec_biasadj_raw_monthly_avg = monthly_avg_2darray(gcm_prec_biasadj_raw[:, 0 : ref_prec.shape[1]]) + gcm_prec_biasadj_raw_monthly_std = monthly_std_2darray(gcm_prec_biasadj_raw[:, 0 : ref_prec.shape[1]]) # Calculate value compared to mean and standard deviation gcm_prec_biasadj_zscore = ( - gcm_prec_biasadj_raw - - np.tile(gcm_prec_biasadj_raw_monthly_avg, int(bc_prec.shape[1] / 12)) + gcm_prec_biasadj_raw - np.tile(gcm_prec_biasadj_raw_monthly_avg, int(bc_prec.shape[1] / 12)) ) / np.tile(gcm_prec_biasadj_raw_monthly_std, int(bc_prec.shape[1] / 12)) gcm_prec_biasadj = np.tile( gcm_prec_biasadj_raw_monthly_avg, int(bc_prec.shape[1] / 12) - ) + gcm_prec_biasadj_zscore * np.tile( - ref_prec_monthly_std, int(bc_prec.shape[1] / 12) - ) + ) + gcm_prec_biasadj_zscore * np.tile(ref_prec_monthly_std, int(bc_prec.shape[1] / 12)) gcm_prec_biasadj[gcm_prec_biasadj < 0] = 0 # Identify outliers using reference's monthly maximum adjusted for future increases @@ -385,19 +319,15 @@ def prec_biasadj_opt1( roll_amt, axis=1, ) - gcm_prec_max_check = np.tile( - ref_prec_monthly_max, int(gcm_prec_biasadj.shape[1] / 12) - ) + gcm_prec_max_check = np.tile(ref_prec_monthly_max, int(gcm_prec_biasadj.shape[1] / 12)) # For wetter years in future, adjust monthly max by the annual increase in precipitation gcm_prec_annual = annual_sum_2darray(bc_prec) gcm_prec_annual_norm = gcm_prec_annual / gcm_prec_annual.mean(1)[:, np.newaxis] - gcm_prec_annual_norm_repeated = np.repeat(gcm_prec_annual_norm, 12).reshape( - gcm_prec_biasadj.shape - ) + gcm_prec_annual_norm_repeated = np.repeat(gcm_prec_annual_norm, 12).reshape(gcm_prec_biasadj.shape) gcm_prec_max_check_adj = gcm_prec_max_check * gcm_prec_annual_norm_repeated - gcm_prec_max_check_adj[gcm_prec_max_check_adj < gcm_prec_max_check] = ( - gcm_prec_max_check[gcm_prec_max_check_adj < gcm_prec_max_check] - ) + gcm_prec_max_check_adj[gcm_prec_max_check_adj < gcm_prec_max_check] = gcm_prec_max_check[ + gcm_prec_max_check_adj < gcm_prec_max_check + ] # Replace outliers with monthly mean adjusted for the normalized annual variation outlier_replacement = gcm_prec_annual_norm_repeated * np.tile( @@ -411,16 +341,10 @@ def prec_biasadj_opt1( gcm_elev_biasadj = ref_elev # Assertion that bias adjustment does not drastically modify the precipitation and are reasonable - gcm_prec_biasadj_subset = gcm_prec_biasadj[ - :, gcm_subset_idx_start : gcm_subset_idx_end + 1 - ] + gcm_prec_biasadj_subset = gcm_prec_biasadj[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] gcm_prec_biasadj_frac = gcm_prec_biasadj_subset.sum(axis=1) / ref_prec.sum(axis=1) - assert gcm_prec_biasadj.max() <= 10, ( - 'gcm_prec_adj (precipitation bias adjustment) too high, needs to be modified' - ) - assert gcm_prec_biasadj.min() >= 0, ( - 'gcm_prec_adj is producing a negative precipitation value' - ) + assert gcm_prec_biasadj.max() <= 10, 'gcm_prec_adj (precipitation bias adjustment) too high, needs to be modified' + assert gcm_prec_biasadj.min() >= 0, 'gcm_prec_adj is producing a negative precipitation value' return gcm_prec_biasadj, gcm_elev_biasadj, gcm_prec_biasadj_frac @@ -469,12 +393,8 @@ def temp_biasadj_QDM( new gcm elevation is the elevation of the reference climate dataset """ # GCM historic subset to agree with reference time period to enable QDM bias correction - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] - gcm_subset_idx_end = np.where( - dates_table.date.values == dates_table_ref.date.values[-1] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] + gcm_subset_idx_end = np.where(dates_table.date.values == dates_table_ref.date.values[-1])[0][0] gcm_temp_historic = gcm_temp[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] # Convert to Kelvin @@ -497,9 +417,7 @@ def temp_biasadj_QDM( # create an empty array for the bias-corrected GCM data # gcm_temp_biasadj = np.zeros(bc_temp.size) loop_years = 20 # number of years used for each bias-correction period - loop_months = ( - loop_years * 12 - ) # number of months used for each bias-correction period + loop_months = loop_years * 12 # number of months used for each bias-correction period # convert to Kelvin to better handle Celsius values around 0) bc_temp = bc_temp + 273.15 @@ -508,9 +426,7 @@ def temp_biasadj_QDM( for j in range(0, len(bc_temp)): gcm_temp_biasadj = [] # empty list for bias-corrected data - bc_loops = ( - len(bc_temp[j]) / loop_months - ) # determine number of loops needed for bias-correction + bc_loops = len(bc_temp[j]) / loop_months # determine number of loops needed for bias-correction # loop through however many times are required to bias-correct the entire time period # using smaller time periods (typically 20-30 years) to better capture the @@ -522,9 +438,9 @@ def temp_biasadj_QDM( # now loop through each individual value within the time period for bias correction for ival, projected_value in enumerate(bc_temp_loop): percentile = percentileofscore(bc_temp_loop, projected_value) - bias_correction_factor = np.percentile( - ref_temp, percentile - ) / np.percentile(gcm_temp_historic, percentile) + bias_correction_factor = np.percentile(ref_temp, percentile) / np.percentile( + gcm_temp_historic, percentile + ) bc_temp_loop_corrected[ival] = projected_value * bias_correction_factor # append the values from each time period to a list gcm_temp_biasadj.append(bc_temp_loop_corrected) @@ -592,12 +508,8 @@ def prec_biasadj_QDM( """ # GCM historic subset to agree with reference time period to enable QDM bias correction - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] - gcm_subset_idx_end = np.where( - dates_table.date.values == dates_table_ref.date.values[-1] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] + gcm_subset_idx_end = np.where(dates_table.date.values == dates_table_ref.date.values[-1])[0][0] gcm_prec_historic = gcm_prec[:, gcm_subset_idx_start : gcm_subset_idx_end + 1] # if/else statement for whether or not the full GCM period is the same as the simulation period @@ -616,18 +528,14 @@ def prec_biasadj_QDM( # create an empty array for the bias-corrected GCM data # gcm_prec_biasadj = np.zeros(bc_prec.size) loop_years = 20 # number of years used for each bias-correction period - loop_months = ( - loop_years * 12 - ) # number of months used for each bias-correction period + loop_months = loop_years * 12 # number of months used for each bias-correction period # bc_prec = bc_prec[0] all_gcm_prec_biasadj = [] # empty list for all glaciers for j in range(0, len(bc_prec)): gcm_prec_biasadj = [] # empty list for bias-corrected data - bc_loops = ( - len(bc_prec[j]) / loop_months - ) # determine number of loops needed for bias-correction + bc_loops = len(bc_prec[j]) / loop_months # determine number of loops needed for bias-correction # loop through however many times are required to bias-correct the entire time period # using smaller time periods (typically 20-30 years) to better capture the @@ -639,9 +547,9 @@ def prec_biasadj_QDM( # now loop through each individual value within the time period for bias correction for ival, projected_value in enumerate(bc_prec_loop): percentile = percentileofscore(bc_prec_loop, projected_value) - bias_correction_factor = np.percentile( - ref_prec, percentile - ) / np.percentile(gcm_prec_historic, percentile) + bias_correction_factor = np.percentile(ref_prec, percentile) / np.percentile( + gcm_prec_historic, percentile + ) bc_prec_loop_corrected[ival] = projected_value * bias_correction_factor # append the values from each time period to a list gcm_prec_biasadj.append(bc_prec_loop_corrected) @@ -658,9 +566,7 @@ def prec_biasadj_QDM( return gcm_prec_biasadj, gcm_elev_biasadj -def monthly_avg_array_rolled( - ref_array, dates_table_ref, dates_table, sim_startyear, ref_startyear -): +def monthly_avg_array_rolled(ref_array, dates_table_ref, dates_table, sim_startyear, ref_startyear): """Monthly average array from reference data rolled to ensure proper months Parameters @@ -678,9 +584,7 @@ def monthly_avg_array_rolled( gcm climate data based on monthly average of reference data """ # GCM subset to agree with reference time period to calculate bias corrections - gcm_subset_idx_start = np.where( - dates_table.date.values == dates_table_ref.date.values[0] - )[0][0] + gcm_subset_idx_start = np.where(dates_table.date.values == dates_table_ref.date.values[0])[0][0] # Roll months so they are aligned with simulation months roll_amt = -1 * (12 - gcm_subset_idx_start % 12) diff --git a/pygem/glacierdynamics.py b/pygem/glacierdynamics.py index 02b607df..b4ca6bdb 100755 --- a/pygem/glacierdynamics.py +++ b/pygem/glacierdynamics.py @@ -105,9 +105,7 @@ def __init__( self.calving_k = cfg.PARAMS['calving_k'] self.calving_m3_since_y0 = 0.0 # total calving since time y0 - assert len(flowlines) == 1, ( - 'MassRedistributionCurveModel is not set up for multiple flowlines' - ) + assert len(flowlines) == 1, 'MassRedistributionCurveModel is not set up for multiple flowlines' def run_until(self, y1, run_single_year=False): """Runs the model from the current year up to a given year date y1. @@ -133,17 +131,13 @@ def run_until(self, y1, run_single_year=False): # Check for domain bounds if self.check_for_boundaries: if self.fls[-1].thick[-1] > 10: - raise RuntimeError( - 'Glacier exceeds domain boundaries, at year: {}'.format(self.yr) - ) + raise RuntimeError('Glacier exceeds domain boundaries, at year: {}'.format(self.yr)) # Check for NaNs for fl in self.fls: if np.any(~np.isfinite(fl.thick)): raise FloatingPointError('NaN in numerical solution.') - def run_until_and_store( - self, y1, run_path=None, diag_path=None, store_monthly_step=None - ): + def run_until_and_store(self, y1, run_path=None, diag_path=None, store_monthly_step=None): """Runs the model and returns intermediate steps in xarray datasets. This function repeatedly calls FlowlineModel.run_until for either @@ -176,16 +170,10 @@ def run_until_and_store( """ if int(y1) != y1: - raise InvalidParamsError( - 'run_until_and_store only accepts integer year dates.' - ) + raise InvalidParamsError('run_until_and_store only accepts integer year dates.') if not self.mb_model.hemisphere: - raise InvalidParamsError( - 'run_until_and_store needs a ' - 'mass-balance model with an unambiguous ' - 'hemisphere.' - ) + raise InvalidParamsError('run_until_and_store needs a mass-balance model with an unambiguous hemisphere.') # time yearly_time = np.arange(np.floor(self.yr), np.floor(y1) + 1) @@ -256,15 +244,11 @@ def run_until_and_store( diag_ds['length_m'].attrs['description'] = 'Glacier length' diag_ds['length_m'].attrs['unit'] = 'm 3' diag_ds['ela_m'] = ('time', np.zeros(nm) * np.nan) - diag_ds['ela_m'].attrs['description'] = ( - 'Annual Equilibrium Line Altitude (ELA)' - ) + diag_ds['ela_m'].attrs['description'] = 'Annual Equilibrium Line Altitude (ELA)' diag_ds['ela_m'].attrs['unit'] = 'm a.s.l' if self.is_tidewater: diag_ds['calving_m3'] = ('time', np.zeros(nm) * np.nan) - diag_ds['calving_m3'].attrs['description'] = ( - 'Total accumulated calving flux' - ) + diag_ds['calving_m3'].attrs['description'] = 'Total accumulated calving flux' diag_ds['calving_m3'].attrs['unit'] = 'm 3' diag_ds['calving_rate_myr'] = ('time', np.zeros(nm) * np.nan) diag_ds['calving_rate_myr'].attrs['description'] = 'Calving rate' @@ -316,15 +300,11 @@ def run_until_and_store( ds.attrs['creation_date'] = strftime('%Y-%m-%d %H:%M:%S', gmtime()) ds.coords['time'] = yearly_time ds['time'].attrs['description'] = 'Floating hydrological year' - varcoords = OrderedDict( - time=('time', yearly_time), year=('time', yearly_time) - ) + varcoords = OrderedDict(time=('time', yearly_time), year=('time', yearly_time)) ds['ts_section'] = xr.DataArray(s, dims=('time', 'x'), coords=varcoords) ds['ts_width_m'] = xr.DataArray(w, dims=('time', 'x'), coords=varcoords) if self.is_tidewater: - ds['ts_calving_bucket_m3'] = xr.DataArray( - b, dims=('time',), coords=varcoords - ) + ds['ts_calving_bucket_m3'] = xr.DataArray(b, dims=('time',), coords=varcoords) run_ds.append(ds) # write output? @@ -359,11 +339,7 @@ def updategeometry(self, year, debug=False): # CONSTANT AREAS # Mass redistribution ignored for calibration and spinup years (glacier properties constant) - if ( - (self.option_areaconstant) - or (year < self.spinupyears) - or (year < self.constantarea_years) - ): + if (self.option_areaconstant) or (year < self.spinupyears) or (year < self.constantarea_years): # run mass balance glac_bin_massbalclim_annual = self.mb_model.get_annual_mb( heights, fls=self.fls, fl_id=fl_id, year=year, debug=False @@ -373,9 +349,7 @@ def updategeometry(self, year, debug=False): # FRONTAL ABLATION if self.is_tidewater: # Frontal ablation (m3 ice) - fa_m3 = self._get_annual_frontalablation( - heights, fls=self.fls, fl_id=fl_id, year=year, debug=False - ) + fa_m3 = self._get_annual_frontalablation(heights, fls=self.fls, fl_id=fl_id, year=year, debug=False) if debug: print('fa_m3_init:', fa_m3) vol_init = (self.fls[fl_id].section * fl.dx_meter).sum() @@ -384,9 +358,7 @@ def updategeometry(self, year, debug=False): # First, remove volume lost to frontal ablation # changes to _t0 not _t1, since t1 will be done in the mass redistribution - glac_idx_bsl = np.where( - (thick_t0 > 0) & (fl.bed_h < self.water_level) - )[0] + glac_idx_bsl = np.where((thick_t0 > 0) & (fl.bed_h < self.water_level))[0] while fa_m3 > 0 and len(glac_idx_bsl) > 0: if debug: print('fa_m3_remaining:', fa_m3) @@ -408,9 +380,7 @@ def updategeometry(self, year, debug=False): # If frontal ablation more than bin volume, remove entire bin if fa_m3 > vol_last: # Record frontal ablation (m3 w.e.) in mass balance model for output - self.mb_model.glac_bin_frontalablation[ - last_idx, int(12 * (year + 1) - 1) - ] = ( + self.mb_model.glac_bin_frontalablation[last_idx, int(12 * (year + 1) - 1)] = ( vol_last * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] @@ -424,14 +394,10 @@ def updategeometry(self, year, debug=False): # Otherwise, remove ice from the section else: # Update section to remove frontal ablation - section_t0[last_idx] = ( - section_t0[last_idx] - fa_m3 / fl.dx_meter - ) + section_t0[last_idx] = section_t0[last_idx] - fa_m3 / fl.dx_meter self.fls[fl_id].section = section_t0 # Record frontal ablation(m3 w.e.) - self.mb_model.glac_bin_frontalablation[ - last_idx, int(12 * (year + 1) - 1) - ] = ( + self.mb_model.glac_bin_frontalablation[last_idx, int(12 * (year + 1) - 1)] = ( fa_m3 * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] @@ -457,9 +423,7 @@ def updategeometry(self, year, debug=False): (self.fls[fl_id].section * fl.dx_meter).sum(), ) - glac_idx_bsl = np.where( - (thick_t0 > 0) & (fl.bed_h < self.water_level) - )[0] + glac_idx_bsl = np.where((thick_t0 > 0) & (fl.bed_h < self.water_level))[0] # Redistribute mass if glacier was not fully removed by frontal ablation if len(section_t0.nonzero()[0]) > 0: @@ -469,9 +433,7 @@ def updategeometry(self, year, debug=False): heights, fls=self.fls, fl_id=fl_id, year=year, debug=False ) sec_in_year = ( - self.mb_model.dates_table.loc[ - 12 * year : 12 * (year + 1) - 1, 'daysinmonth' - ].values.sum() + self.mb_model.dates_table.loc[12 * year : 12 * (year + 1) - 1, 'daysinmonth'].values.sum() * 24 * 3600 ) @@ -507,23 +469,17 @@ def updategeometry(self, year, debug=False): # Record glacier properties (volume [m3], area [m2], thickness [m], width [km]) # record the next year's properties as well # 'year + 1' used so the glacier properties are consistent with mass balance computations - year = int( - year - ) # required to ensure proper indexing with run_until_and_store (10/21/2020) + year = int(year) # required to ensure proper indexing with run_until_and_store (10/21/2020) glacier_area = fl.widths_m * fl.dx_meter glacier_area[fl.thick == 0] = 0 self.mb_model.glac_bin_area_annual[:, year + 1] = glacier_area self.mb_model.glac_bin_icethickness_annual[:, year + 1] = fl.thick self.mb_model.glac_bin_width_annual[:, year + 1] = fl.widths_m self.mb_model.glac_wide_area_annual[year + 1] = glacier_area.sum() - self.mb_model.glac_wide_volume_annual[year + 1] = ( - fl.section * fl.dx_meter - ).sum() + self.mb_model.glac_wide_volume_annual[year + 1] = (fl.section * fl.dx_meter).sum() # %% ----- FRONTAL ABLATION ----- - def _get_annual_frontalablation( - self, heights, year=None, fls=None, fl_id=None, calving_k=None, debug=False - ): + def _get_annual_frontalablation(self, heights, year=None, fls=None, fl_id=None, calving_k=None, debug=False): """Calculate frontal ablation for a given year Returns frontal ablation (m3 ice) @@ -544,9 +500,7 @@ def _get_annual_frontalablation( # Ice thickness (average) if fl_section is not None and fl_widths_m is not None: icethickness_t0 = np.zeros(fl_section.shape) - icethickness_t0[fl_widths_m > 0] = ( - fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] - ) + icethickness_t0[fl_widths_m > 0] = fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] else: icethickness_t0 = None @@ -562,13 +516,9 @@ def _get_annual_frontalablation( q_calving = 0 if glacier_area_t0.sum() > 0: try: - last_above_wl = np.nonzero( - (fl.surface_h > self.water_level) & (fl.thick > 0) - )[0][-1] + last_above_wl = np.nonzero((fl.surface_h > self.water_level) & (fl.thick > 0))[0][-1] except: - last_above_wl = np.nonzero( - (fl.bed_h <= self.water_level) & (fl.thick > 0) - )[0][-1] + last_above_wl = np.nonzero((fl.bed_h <= self.water_level) & (fl.thick > 0))[0][-1] if last_above_wl is not None: if fl.bed_h[last_above_wl] < self.water_level: @@ -598,9 +548,7 @@ def _get_annual_frontalablation( q_calving = k * d * h * fl.widths_m[last_above_wl] # Max frontal ablation is removing all bins with bed below water level - glac_idx_bsl = np.where( - (fl.thick > 0) & (fl.bed_h < self.water_level) - )[0] + glac_idx_bsl = np.where((fl.thick > 0) & (fl.bed_h < self.water_level))[0] q_calving_max = np.sum(section[glac_idx_bsl]) * fl.dx_meter if q_calving > q_calving_max + pygem_prms['constants']['tolerance']: @@ -660,9 +608,7 @@ def _massredistributionHuss( # Annual glacier-wide volume change [m3] # units: [m ice / s] * [s] * [m2] = m3 ice - glacier_volumechange = ( - glac_bin_massbalclim_annual * sec_in_year * glacier_area_t0 - ).sum() + glacier_volumechange = (glac_bin_massbalclim_annual * sec_in_year * glacier_area_t0).sum() if debug: print('\nDebugging Mass Redistribution Huss function\n') @@ -680,17 +626,15 @@ def _massredistributionHuss( glac_idx_t0 = self.fls[0].thick.nonzero()[0] # Compute ice thickness [m ice], glacier area [m2], ice thickness change [m ice] after redistribution - icethickness_change, glacier_volumechange_remaining = ( - self._massredistributioncurveHuss( - section_t0, - thick_t0, - width_t0, - glac_idx_t0, - glacier_volumechange, - glac_bin_massbalclim_annual, - heights, - debug=False, - ) + icethickness_change, glacier_volumechange_remaining = self._massredistributioncurveHuss( + section_t0, + thick_t0, + width_t0, + glac_idx_t0, + glacier_volumechange, + glac_bin_massbalclim_annual, + heights, + debug=False, ) if debug: print( @@ -712,9 +656,7 @@ def _massredistributionHuss( section_t0_retreated = self.fls[0].section.copy() thick_t0_retreated = self.fls[0].thick.copy() width_t0_retreated = self.fls[0].widths_m.copy() - glacier_volumechange_remaining_retreated = ( - glacier_volumechange_remaining.copy() - ) + glacier_volumechange_remaining_retreated = glacier_volumechange_remaining.copy() glac_idx_t0_retreated = thick_t0_retreated.nonzero()[0] glacier_area_t0_retreated = width_t0_retreated * self.fls[0].dx_meter glacier_area_t0_retreated[thick_t0 == 0] = 0 @@ -722,23 +664,19 @@ def _massredistributionHuss( # distribute the remaining glacier volume change over the entire glacier (remaining bins) massbalclim_retreat = np.zeros(thick_t0_retreated.shape) massbalclim_retreat[glac_idx_t0_retreated] = ( - glacier_volumechange_remaining - / glacier_area_t0_retreated.sum() - / sec_in_year + glacier_volumechange_remaining / glacier_area_t0_retreated.sum() / sec_in_year ) # Mass redistribution # apply mass redistribution using Huss' empirical geometry change equations - icethickness_change, glacier_volumechange_remaining = ( - self._massredistributioncurveHuss( - section_t0_retreated, - thick_t0_retreated, - width_t0_retreated, - glac_idx_t0_retreated, - glacier_volumechange_remaining_retreated, - massbalclim_retreat, - heights, - debug=False, - ) + icethickness_change, glacier_volumechange_remaining = self._massredistributioncurveHuss( + section_t0_retreated, + thick_t0_retreated, + width_t0_retreated, + glac_idx_t0_retreated, + glacier_volumechange_remaining_retreated, + massbalclim_retreat, + heights, + debug=False, ) # Avoid rounding errors that get loop stuck if abs(glacier_volumechange_remaining) < 1: @@ -763,9 +701,7 @@ def _massredistributionHuss( # 2. If additional volume after adding new bin, then redistribute mass gain across all bins again, # i.e., increase the ice thickness and width # 3. Repeat adding a new bin and redistributing the mass until no addiitonal volume is left - while ( - icethickness_change > pygem_prms['sim']['icethickness_advancethreshold'] - ).any() == True: + while (icethickness_change > pygem_prms['sim']['icethickness_advancethreshold']).any() == True: if debug: print('advancing glacier') @@ -781,25 +717,17 @@ def _massredistributionHuss( print('width_t0_raw:', width_t0_raw, '\n\n') # Index bins that are advancing - icethickness_change[ - icethickness_change - <= pygem_prms['sim']['icethickness_advancethreshold'] - ] = 0 + icethickness_change[icethickness_change <= pygem_prms['sim']['icethickness_advancethreshold']] = 0 glac_idx_advance = icethickness_change.nonzero()[0] # Update ice thickness based on maximum advance threshold [m ice] - self.fls[0].thick[glac_idx_advance] = self.fls[0].thick[ - glac_idx_advance - ] - ( - icethickness_change[glac_idx_advance] - - pygem_prms['sim']['icethickness_advancethreshold'] + self.fls[0].thick[glac_idx_advance] = self.fls[0].thick[glac_idx_advance] - ( + icethickness_change[glac_idx_advance] - pygem_prms['sim']['icethickness_advancethreshold'] ) glacier_area_t1 = self.fls[0].widths_m.copy() * self.fls[0].dx_meter # Advance volume [m3] - advance_volume = ( - glacier_area_t0_raw[glac_idx_advance] * thick_t0_raw[glac_idx_advance] - ).sum() - ( + advance_volume = (glacier_area_t0_raw[glac_idx_advance] * thick_t0_raw[glac_idx_advance]).sum() - ( glacier_area_t1[glac_idx_advance] * self.fls[0].thick[glac_idx_advance] ).sum() @@ -834,19 +762,12 @@ def _massredistributionHuss( 'thickness:', self.fls[0].thick[glac_idx_t0_term], ) - print( - np.where( - self.fls[0].surface_h[glac_idx_t0] - > self.fls[0].surface_h[glac_idx_t0_term] - )[0] - ) + print(np.where(self.fls[0].surface_h[glac_idx_t0] > self.fls[0].surface_h[glac_idx_t0_term])[0]) print('advance section:', advance_section) thick_prior = np.copy(self.fls[0].thick) section_updated = np.copy(self.fls[0].section) - section_updated[glac_idx_t0_term] = ( - section_updated[glac_idx_t0_term] + advance_section - ) + section_updated[glac_idx_t0_term] = section_updated[glac_idx_t0_term] + advance_section if debug: print( @@ -883,9 +804,7 @@ def _massredistributionHuss( width_t0_raw = self.fls[0].widths_m.copy() glacier_area_t0_raw = width_t0_raw * self.fls[0].dx_meter - thick_reduction = ( - self.fls[0].surface_h[glac_idx_t0_term] - elev_term - ) + thick_reduction = self.fls[0].surface_h[glac_idx_t0_term] - elev_term if debug: print('thick_reduction:', thick_reduction) @@ -895,18 +814,12 @@ def _massredistributionHuss( self.fls[0].section[glac_idx_t0_term], ) - self.fls[0].thick[glac_idx_t0_term] = ( - self.fls[0].thick[glac_idx_t0_term] - thick_reduction - ) + self.fls[0].thick[glac_idx_t0_term] = self.fls[0].thick[glac_idx_t0_term] - thick_reduction glacier_area_t1 = self.fls[0].widths_m.copy() * self.fls[0].dx_meter # Advance volume [m3] - advance_volume = ( - glacier_area_t0_raw[glac_idx_t0_term] - * thick_t0_raw[glac_idx_t0_term] - ).sum() - ( - glacier_area_t1[glac_idx_t0_term] - * self.fls[0].thick[glac_idx_t0_term] + advance_volume = (glacier_area_t0_raw[glac_idx_t0_term] * thick_t0_raw[glac_idx_t0_term]).sum() - ( + glacier_area_t1[glac_idx_t0_term] * self.fls[0].thick[glac_idx_t0_term] ).sum() if debug: @@ -924,10 +837,7 @@ def _massredistributionHuss( if advance_volume > 0: glac_idx_bin2add = np.where( - self.fls[0].surface_h - == self.fls[0] - .surface_h[np.where(self.fls[0].surface_h < min_elev)[0]] - .max() + self.fls[0].surface_h == self.fls[0].surface_h[np.where(self.fls[0].surface_h < min_elev)[0]].max() )[0][0] section_2add = self.fls[0].section.copy() section_2add[glac_idx_bin2add] = advance_section @@ -952,43 +862,30 @@ def _massredistributionHuss( # Average area of glacier terminus [m2] # exclude the bin at the terminus, since this bin may need to be filled first try: - minelev_idx = np.where(heights == heights[glac_idx_terminus].min())[ - 0 - ][0] + minelev_idx = np.where(heights == heights[glac_idx_terminus].min())[0][0] glac_idx_terminus_removemin = list(glac_idx_terminus) glac_idx_terminus_removemin.remove(minelev_idx) - terminus_thickness_avg = np.mean( - self.fls[0].thick[glac_idx_terminus_removemin] - ) + terminus_thickness_avg = np.mean(self.fls[0].thick[glac_idx_terminus_removemin]) except: glac_idx_terminus_initial = glac_idx_initial[ (heights[glac_idx_initial] - heights[glac_idx_initial].min()) - / ( - heights[glac_idx_initial].max() - - heights[glac_idx_initial].min() - ) + / (heights[glac_idx_initial].max() - heights[glac_idx_initial].min()) * 100 < pygem_prms['sim']['terminus_percentage'] ] if glac_idx_terminus_initial.shape[0] <= 1: glac_idx_terminus_initial = glac_idx_initial.copy() - minelev_idx = np.where( - heights == heights[glac_idx_terminus_initial].min() - )[0][0] + minelev_idx = np.where(heights == heights[glac_idx_terminus_initial].min())[0][0] glac_idx_terminus_removemin = list(glac_idx_terminus_initial) glac_idx_terminus_removemin.remove(minelev_idx) - terminus_thickness_avg = np.mean( - self.fls[0].thick[glac_idx_terminus_removemin] - ) + terminus_thickness_avg = np.mean(self.fls[0].thick[glac_idx_terminus_removemin]) # If last bin exceeds terminus thickness average then fill up the bin to average and redistribute mass if self.fls[0].thick[glac_idx_bin2add] > terminus_thickness_avg: self.fls[0].thick[glac_idx_bin2add] = terminus_thickness_avg # Redistribute remaining mass - volume_added2bin = ( - self.fls[0].section[glac_idx_bin2add] * self.fls[0].dx_meter - ) + volume_added2bin = self.fls[0].section[glac_idx_bin2add] * self.fls[0].dx_meter advance_volume -= volume_added2bin # With remaining advance volume, add a bin or redistribute over existing bins if no bins left @@ -1014,21 +911,17 @@ def _massredistributionHuss( glacier_area_t0 = self.fls[0].widths_m.copy() * self.fls[0].dx_meter glac_bin_massbalclim_annual = np.zeros(self.fls[0].thick.shape) glac_bin_massbalclim_annual[glac_idx_t0] = ( - glacier_volumechange_remaining - / glacier_area_t0.sum() - / sec_in_year + glacier_volumechange_remaining / glacier_area_t0.sum() / sec_in_year ) - icethickness_change, glacier_volumechange_remaining = ( - self._massredistributioncurveHuss( - self.fls[0].section.copy(), - self.fls[0].thick.copy(), - self.fls[0].widths_m.copy(), - glac_idx_t0, - advance_volume, - glac_bin_massbalclim_annual, - heights, - debug=False, - ) + icethickness_change, glacier_volumechange_remaining = self._massredistributioncurveHuss( + self.fls[0].section.copy(), + self.fls[0].thick.copy(), + self.fls[0].widths_m.copy(), + glac_idx_t0, + advance_volume, + glac_bin_massbalclim_annual, + heights, + debug=False, ) def _massredistributioncurveHuss( @@ -1090,17 +983,15 @@ def _massredistributioncurveHuss( icethicknesschange_norm = np.zeros(glacier_area_t0.shape) # Normalized elevation range [-] # (max elevation - bin elevation) / (max_elevation - min_elevation) - elevrange_norm[glacier_area_t0 > 0] = ( - heights[glac_idx_t0].max() - heights[glac_idx_t0] - ) / (heights[glac_idx_t0].max() - heights[glac_idx_t0].min()) + elevrange_norm[glacier_area_t0 > 0] = (heights[glac_idx_t0].max() - heights[glac_idx_t0]) / ( + heights[glac_idx_t0].max() - heights[glac_idx_t0].min() + ) # using indices as opposed to elevations automatically skips bins on the glacier that have no area # such that the normalization is done only on bins where the glacier lies # Normalized ice thickness change [-] icethicknesschange_norm[glacier_area_t0 > 0] = ( - (elevrange_norm[glacier_area_t0 > 0] + a) ** gamma - + b * (elevrange_norm[glacier_area_t0 > 0] + a) - + c + (elevrange_norm[glacier_area_t0 > 0] + a) ** gamma + b * (elevrange_norm[glacier_area_t0 > 0] + a) + c ) # delta_h = (h_n + a)**gamma + b*(h_n + a) + c # indexing is faster here @@ -1109,9 +1000,7 @@ def _massredistributioncurveHuss( icethicknesschange_norm[icethicknesschange_norm < 0] = 0 # Huss' ice thickness scaling factor, fs_huss [m ice] # units: m3 / (m2 * [-]) * (1000 m / 1 km) = m ice - fs_huss = ( - glacier_volumechange / (glacier_area_t0 * icethicknesschange_norm).sum() - ) + fs_huss = glacier_volumechange / (glacier_area_t0 * icethicknesschange_norm).sum() if debug: print('fs_huss:', fs_huss) # Volume change [m3 ice] @@ -1141,13 +1030,10 @@ def _massredistributioncurveHuss( # Compute the remaining volume change bin_volumechange_remaining = bin_volumechange - ( - self.fls[0].section * self.fls[0].dx_meter - - section_t0 * self.fls[0].dx_meter + self.fls[0].section * self.fls[0].dx_meter - section_t0 * self.fls[0].dx_meter ) # remove values below tolerance to avoid rounding errors - bin_volumechange_remaining[ - abs(bin_volumechange_remaining) < pygem_prms['constants']['tolerance'] - ] = 0 + bin_volumechange_remaining[abs(bin_volumechange_remaining) < pygem_prms['constants']['tolerance']] = 0 # Glacier volume change remaining - if less than zero, then needed for retreat glacier_volumechange_remaining = bin_volumechange_remaining.sum() diff --git a/pygem/massbalance.py b/pygem/massbalance.py index 36e9d308..60ebdb1f 100644 --- a/pygem/massbalance.py +++ b/pygem/massbalance.py @@ -82,11 +82,7 @@ def __init__( self.width_initial = fls[fl_id].widths_m self.glacier_area_initial = fls[fl_id].widths_m * fls[fl_id].dx_meter self.heights = fls[fl_id].surface_h - if ( - pygem_prms['mb']['include_debris'] - and not ignore_debris - and not gdir.is_tidewater - ): + if pygem_prms['mb']['include_debris'] and not ignore_debris and not gdir.is_tidewater: try: self.debris_ed = fls[fl_id].debris_ed except: @@ -130,12 +126,8 @@ def __init__( self.glac_bin_massbalclim_annual = np.zeros((nbins, self.nyears)) self.glac_bin_surfacetype_annual = np.zeros((nbins, self.nyears + 1)) self.glac_bin_area_annual = np.zeros((nbins, self.nyears + 1)) - self.glac_bin_icethickness_annual = np.zeros( - (nbins, self.nyears + 1) - ) # Needed for MassRedistributionCurves - self.glac_bin_width_annual = np.zeros( - (nbins, self.nyears + 1) - ) # Needed for MassRedistributionCurves + self.glac_bin_icethickness_annual = np.zeros((nbins, self.nyears + 1)) # Needed for MassRedistributionCurves + self.glac_bin_width_annual = np.zeros((nbins, self.nyears + 1)) # Needed for MassRedistributionCurves self.offglac_bin_prec = np.zeros((nbins, self.nmonths)) self.offglac_bin_melt = np.zeros((nbins, self.nmonths)) self.offglac_bin_refreeze = np.zeros((nbins, self.nmonths)) @@ -171,36 +163,28 @@ def __init__( if pygem_prms['mb']['option_refreezing'] == 'HH2015': # Refreezing layers density, volumetric heat capacity, and thermal conductivity self.rf_dens_expb = ( - pygem_prms['mb']['HH2015_rf_opts']['rf_dens_bot'] - / pygem_prms['mb']['HH2015_rf_opts']['rf_dens_top'] + pygem_prms['mb']['HH2015_rf_opts']['rf_dens_bot'] / pygem_prms['mb']['HH2015_rf_opts']['rf_dens_top'] ) ** (1 / (pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1)) self.rf_layers_dens = np.array( [ - pygem_prms['mb']['HH2015_rf_opts']['rf_dens_top'] - * self.rf_dens_expb**x - for x in np.arange( - 0, pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - ) + pygem_prms['mb']['HH2015_rf_opts']['rf_dens_top'] * self.rf_dens_expb**x + for x in np.arange(0, pygem_prms['mb']['HH2015_rf_opts']['rf_layers']) ] ) - self.rf_layers_ch = (1 - self.rf_layers_dens / 1000) * pygem_prms[ - 'constants' - ]['ch_air'] + self.rf_layers_dens / 1000 * pygem_prms['constants']['ch_ice'] - self.rf_layers_k = (1 - self.rf_layers_dens / 1000) * pygem_prms[ - 'constants' - ]['k_air'] + self.rf_layers_dens / 1000 * pygem_prms['constants']['k_ice'] + self.rf_layers_ch = (1 - self.rf_layers_dens / 1000) * pygem_prms['constants'][ + 'ch_air' + ] + self.rf_layers_dens / 1000 * pygem_prms['constants']['ch_ice'] + self.rf_layers_k = (1 - self.rf_layers_dens / 1000) * pygem_prms['constants'][ + 'k_air' + ] + self.rf_layers_dens / 1000 * pygem_prms['constants']['k_ice'] # refreeze in each bin self.refr = np.zeros(nbins) # refrezee cold content or "potential" refreeze self.rf_cold = np.zeros(nbins) # layer temp of each elev bin for present time step - self.te_rf = np.zeros( - (pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths) - ) + self.te_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths)) # layer temp of each elev bin for previous time step - self.tl_rf = np.zeros( - (pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths) - ) + self.tl_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths)) # Sea level for marine-terminating glaciers self.sea_level = 0 @@ -257,9 +241,7 @@ def get_annual_mb( # Ice thickness (average) if fl_section is not None and fl_widths_m is not None: icethickness_t0 = np.zeros(fl_section.shape) - icethickness_t0[fl_widths_m > 0] = ( - fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] - ) + icethickness_t0[fl_widths_m > 0] = fl_section[fl_widths_m > 0] / fl_widths_m[fl_widths_m > 0] else: icethickness_t0 = None @@ -282,9 +264,7 @@ def get_annual_mb( # Refreezing specific layers if pygem_prms['mb']['option_refreezing'] == 'HH2015' and year_idx == 0: self.te_rf[:, :, 0] = 0 # layer temp of each elev bin for present time step - self.tl_rf[:, :, 0] = ( - 0 # layer temp of each elev bin for previous time step - ) + self.tl_rf[:, :, 0] = 0 # layer temp of each elev bin for previous time step elif pygem_prms['mb']['option_refreezing'] == 'Woodward': refreeze_potential = np.zeros(nbins) @@ -293,16 +273,12 @@ def get_annual_mb( # Surface type [0=off-glacier, 1=ice, 2=snow, 3=firn, 4=debris] if year_idx == 0: - self.surfacetype, self.firnline_idx = self._surfacetypebinsinitial( - self.heights - ) + self.surfacetype, self.firnline_idx = self._surfacetypebinsinitial(self.heights) self.glac_bin_surfacetype_annual[:, year_idx] = self.surfacetype # Off-glacier area and indices if option_areaconstant == False: - self.offglac_bin_area_annual[:, year_idx] = ( - glacier_area_initial - glacier_area_t0 - ) + self.offglac_bin_area_annual[:, year_idx] = glacier_area_initial - glacier_area_t0 offglac_idx = np.where(self.offglac_bin_area_annual[:, year_idx] > 0)[0] # Functions currently set up for monthly timestep @@ -317,18 +293,13 @@ def get_annual_mb( self.glacier_gcm_temp[year_start_month_idx:year_stop_month_idx] + self.glacier_gcm_lrgcm[year_start_month_idx:year_stop_month_idx] * ( - self.glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] + self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']] - self.glacier_gcm_elev ) + self.glacier_gcm_lrglac[year_start_month_idx:year_stop_month_idx] - * ( - heights - - self.glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] - )[:, np.newaxis] + * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']])[ + :, np.newaxis + ] + self.modelprms['tbias'] ) @@ -341,12 +312,7 @@ def get_annual_mb( * ( 1 + self.modelprms['precgrad'] - * ( - heights - - self.glacier_rgi_table.loc[ - pygem_prms['mb']['option_elev_ref_downscale'] - ] - ) + * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']]) )[:, np.newaxis] ) # Option to adjust prec of uppermost 25% of glacier for wind erosion and reduced moisture content @@ -370,19 +336,12 @@ def get_annual_mb( height_75 = heights[glac_idx_upper25].min() glac_idx_75 = np.where(heights == height_75)[0][0] # exponential decay - bin_precsnow[ - glac_idx_upper25, year_start_month_idx:year_stop_month_idx - ] = ( - bin_precsnow[ - glac_idx_75, year_start_month_idx:year_stop_month_idx - ] + bin_precsnow[glac_idx_upper25, year_start_month_idx:year_stop_month_idx] = ( + bin_precsnow[glac_idx_75, year_start_month_idx:year_stop_month_idx] * np.exp( -1 * (heights[glac_idx_upper25] - height_75) - / ( - heights[glac_idx_upper25].max() - - heights[glac_idx_upper25].min() - ) + / (heights[glac_idx_upper25].max() - heights[glac_idx_upper25].min()) )[:, np.newaxis] ) # Precipitation cannot be less than 87.5% of the maximum accumulation elsewhere on the glacier @@ -407,8 +366,7 @@ def get_annual_mb( > self.modelprms['tsnow_threshold'] ] ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - > self.modelprms['tsnow_threshold'] + self.bin_temp[:, year_start_month_idx:year_stop_month_idx] > self.modelprms['tsnow_threshold'] ] # if temperature below threshold, then snow ( @@ -417,8 +375,7 @@ def get_annual_mb( <= self.modelprms['tsnow_threshold'] ] ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - <= self.modelprms['tsnow_threshold'] + self.bin_temp[:, year_start_month_idx:year_stop_month_idx] <= self.modelprms['tsnow_threshold'] ] elif pygem_prms['mb']['option_accumulation'] == 2: # if temperature between min/max, then mix of snow/rain using linear relationship between min/max @@ -477,17 +434,13 @@ def get_annual_mb( if step == 0: self.bin_snowpack[:, step] = self.bin_acc[:, step] else: - self.bin_snowpack[:, step] = ( - self.snowpack_remaining[:, step - 1] + self.bin_acc[:, step] - ) + self.bin_snowpack[:, step] = self.snowpack_remaining[:, step - 1] + self.bin_acc[:, step] # MELT [m w.e.] # energy available for melt [degC day] if pygem_prms['mb']['option_ablation'] == 1: # option 1: energy based on monthly temperature - melt_energy_available = ( - self.bin_temp[:, step] * self.dayspermonth[step] - ) + melt_energy_available = self.bin_temp[:, step] * self.dayspermonth[step] melt_energy_available[melt_energy_available < 0] = 0 elif pygem_prms['mb']['option_ablation'] == 2: # Seed randomness for repeatability, but base it on step to ensure the daily variability is not @@ -505,55 +458,41 @@ def get_annual_mb( axis=0, ) # daily temperature in each bin for the monthly timestep - bin_temp_daily = ( - self.bin_temp[:, step][:, np.newaxis] + bin_tempstd_daily - ) + bin_temp_daily = self.bin_temp[:, step][:, np.newaxis] + bin_tempstd_daily # remove negative values bin_temp_daily[bin_temp_daily < 0] = 0 # Energy available for melt [degC day] = sum of daily energy available melt_energy_available = bin_temp_daily.sum(axis=1) # SNOW MELT [m w.e.] - self.bin_meltsnow[:, step] = ( - self.surfacetype_ddf_dict[2] * melt_energy_available - ) + self.bin_meltsnow[:, step] = self.surfacetype_ddf_dict[2] * melt_energy_available # snow melt cannot exceed the snow depth - self.bin_meltsnow[ - self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step - ] = self.bin_snowpack[ - self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step - ] + self.bin_meltsnow[self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step] = ( + self.bin_snowpack[self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step] + ) # GLACIER MELT (ice and firn) [m w.e.] # energy remaining after snow melt [degC day] melt_energy_available = ( - melt_energy_available - - self.bin_meltsnow[:, step] / self.surfacetype_ddf_dict[2] + melt_energy_available - self.bin_meltsnow[:, step] / self.surfacetype_ddf_dict[2] ) # remove low values of energy available caused by rounding errors in the step above - melt_energy_available[ - abs(melt_energy_available) - < pygem_prms['constants']['tolerance'] - ] = 0 + melt_energy_available[abs(melt_energy_available) < pygem_prms['constants']['tolerance']] = 0 # DDF based on surface type [m w.e. degC-1 day-1] for surfacetype_idx in self.surfacetype_ddf_dict: - self.surfacetype_ddf[self.surfacetype == surfacetype_idx] = ( - self.surfacetype_ddf_dict[surfacetype_idx] - ) + self.surfacetype_ddf[self.surfacetype == surfacetype_idx] = self.surfacetype_ddf_dict[ + surfacetype_idx + ] # Debris enhancement factors in ablation area (debris in accumulation area would submerge) if surfacetype_idx == 1 and pygem_prms['mb']['include_debris']: self.surfacetype_ddf[self.surfacetype == 1] = ( - self.surfacetype_ddf[self.surfacetype == 1] - * self.debris_ed[self.surfacetype == 1] + self.surfacetype_ddf[self.surfacetype == 1] * self.debris_ed[self.surfacetype == 1] ) self.bin_meltglac[glac_idx_t0, step] = ( - self.surfacetype_ddf[glac_idx_t0] - * melt_energy_available[glac_idx_t0] + self.surfacetype_ddf[glac_idx_t0] * melt_energy_available[glac_idx_t0] ) # TOTAL MELT (snow + glacier) # off-glacier need to include melt of refreeze because there are no glacier dynamics, # but on-glacier do not need to account for this (simply assume refreeze has same surface type) - self.bin_melt[:, step] = ( - self.bin_meltglac[:, step] + self.bin_meltsnow[:, step] - ) + self.bin_melt[:, step] = self.bin_meltglac[:, step] + self.bin_meltsnow[:, step] # REFREEZING if pygem_prms['mb']['option_refreezing'] == 'HH2015': @@ -563,45 +502,25 @@ def get_annual_mb( # Refreeze based on heat conduction approach (Huss and Hock 2015) # refreeze time step (s) - rf_dt = ( - 3600 - * 24 - * self.dayspermonth[step] - / pygem_prms['mb']['HH2015_rf_opts']['rf_dsc'] - ) + rf_dt = 3600 * 24 * self.dayspermonth[step] / pygem_prms['mb']['HH2015_rf_opts']['rf_dsc'] - if ( - pygem_prms['mb']['HH2015_rf_opts'][ - 'option_rf_limit_meltsnow' - ] - == 1 - ): + if pygem_prms['mb']['HH2015_rf_opts']['option_rf_limit_meltsnow'] == 1: bin_meltlimit = self.bin_meltsnow.copy() else: bin_meltlimit = self.bin_melt.copy() # Debug lowest bin if self.debug_refreeze: - gidx_debug = np.where( - heights == heights[glac_idx_t0].min() - )[0] + gidx_debug = np.where(heights == heights[glac_idx_t0].min())[0] # Loop through each elevation bin of glacier for nbin, gidx in enumerate(glac_idx_t0): # COMPUTE HEAT CONDUCTION - BUILD COLD RESERVOIR # If no melt, then build up cold reservoir (compute heat conduction) - if ( - self.bin_melt[gidx, step] - < pygem_prms['mb']['HH2015_rf_opts']['rf_meltcrit'] - ): - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): + if self.bin_melt[gidx, step] < pygem_prms['mb']['HH2015_rf_opts']['rf_meltcrit']: + if self.debug_refreeze and gidx == gidx_debug and step < 12: print( - '\nMonth ' - + str(self.dates_table.loc[step, 'month']), + '\nMonth ' + str(self.dates_table.loc[step, 'month']), 'Computing heat conduction', ) @@ -609,89 +528,56 @@ def get_annual_mb( self.refr[gidx] = 0 # Loop through multiple iterations to converge on a solution # -> this will loop through 0, 1, 2 - for h in np.arange( - 0, pygem_prms['mb']['HH2015_rf_opts']['rf_dsc'] - ): + for h in np.arange(0, pygem_prms['mb']['HH2015_rf_opts']['rf_dsc']): # Compute heat conduction in layers (loop through rows) # go from 1 to rf_layers-1 to avoid indexing errors with "j-1" and "j+1" # "j+1" is set to zero, which is fine for temperate glaciers but inaccurate for # cold/polythermal glaciers for j in np.arange( 1, - pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - - 1, + pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1, ): # Assume temperature of first layer equals air temperature # assumption probably wrong, but might still work at annual average # Since next line uses tl_rf for all calculations, set tl_rf[0] to present mean # monthly air temperature to ensure the present calculations are done with the # present time step's air temperature - self.tl_rf[0, gidx, step] = self.bin_temp[ - gidx, step - ] + self.tl_rf[0, gidx, step] = self.bin_temp[gidx, step] # Temperature for each layer self.te_rf[j, gidx, step] = self.tl_rf[ j, gidx, step - ] + rf_dt * self.rf_layers_k[ - j - ] / self.rf_layers_ch[j] / pygem_prms['mb'][ + ] + rf_dt * self.rf_layers_k[j] / self.rf_layers_ch[j] / pygem_prms['mb'][ 'HH2015_rf_opts' ]['rf_dz'] ** 2 * 0.5 * ( - ( - self.tl_rf[j - 1, gidx, step] - - self.tl_rf[j, gidx, step] - ) - - ( - self.tl_rf[j, gidx, step] - - self.tl_rf[j + 1, gidx, step] - ) + (self.tl_rf[j - 1, gidx, step] - self.tl_rf[j, gidx, step]) + - (self.tl_rf[j, gidx, step] - self.tl_rf[j + 1, gidx, step]) ) # Update previous time step - self.tl_rf[:, gidx, step] = self.te_rf[ - :, gidx, step - ] - - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): + self.tl_rf[:, gidx, step] = self.te_rf[:, gidx, step] + + if self.debug_refreeze and gidx == gidx_debug and step < 12: print( 'tl_rf:', - [ - '{:.2f}'.format(x) - for x in self.tl_rf[:, gidx, step] - ], + ['{:.2f}'.format(x) for x in self.tl_rf[:, gidx, step]], ) # COMPUTE REFREEZING - TAP INTO "COLD RESERVOIR" or potential refreezing else: - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): + if self.debug_refreeze and gidx == gidx_debug and step < 12: print( - '\nMonth ' - + str(self.dates_table.loc[step, 'month']), + '\nMonth ' + str(self.dates_table.loc[step, 'month']), 'Computing refreeze', ) # Refreezing over firn surface - if (self.surfacetype[gidx] == 2) or ( - self.surfacetype[gidx] == 3 - ): - nlayers = ( - pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - - 1 - ) + if (self.surfacetype[gidx] == 2) or (self.surfacetype[gidx] == 3): + nlayers = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 # Refreezing over ice surface else: # Approximate number of layers of snow on top of ice smax = np.round( ( - self.bin_snowpack[gidx, step] - / (self.rf_layers_dens[0] / 1000) + self.bin_snowpack[gidx, step] / (self.rf_layers_dens[0] / 1000) + pygem_prms['mb']['HH2015_rf_opts']['pp'] ) / pygem_prms['mb']['HH2015_rf_opts']['rf_dz'], @@ -705,36 +591,14 @@ def get_annual_mb( if smax == 0: self.rf_cold[gidx] = 0 # if smax greater than the number of layers, set to max number of layers minus 1 - if ( - smax - > pygem_prms['mb']['HH2015_rf_opts'][ - 'rf_layers' - ] - - 1 - ): - smax = ( - pygem_prms['mb']['HH2015_rf_opts'][ - 'rf_layers' - ] - - 1 - ) + if smax > pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1: + smax = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 nlayers = int(smax) # Compute potential refreeze, "cold reservoir", from temperature in each layer # only calculate potential refreezing first time it starts melting each year - if ( - self.rf_cold[gidx] == 0 - and self.tl_rf[:, gidx, step].min() < 0 - ): - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): - print( - 'calculating potential refreeze from ' - + str(nlayers) - + ' layers' - ) + if self.rf_cold[gidx] == 0 and self.tl_rf[:, gidx, step].min() < 0: + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print('calculating potential refreeze from ' + str(nlayers) + ' layers') for j in np.arange(0, nlayers): j += 1 @@ -742,19 +606,13 @@ def get_annual_mb( rf_cold_layer = ( self.tl_rf[j, gidx, step] * self.rf_layers_ch[j] - * pygem_prms['mb']['HH2015_rf_opts'][ - 'rf_dz' - ] + * pygem_prms['mb']['HH2015_rf_opts']['rf_dz'] / pygem_prms['constants']['Lh_rf'] / pygem_prms['constants']['density_water'] ) self.rf_cold[gidx] -= rf_cold_layer - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): + if self.debug_refreeze and gidx == gidx_debug and step < 12: print( 'j:', j, @@ -768,25 +626,13 @@ def get_annual_mb( np.round(self.rf_cold[gidx], 2), ) - if ( - self.debug_refreeze - and gidx == gidx_debug - and step < 12 - ): - print( - 'rf_cold:', np.round(self.rf_cold[gidx], 2) - ) + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print('rf_cold:', np.round(self.rf_cold[gidx], 2)) # Compute refreezing # If melt and liquid prec < potential refreeze, then refreeze all melt and liquid prec - if ( - bin_meltlimit[gidx, step] - + self.bin_prec[gidx, step] - ) < self.rf_cold[gidx]: - self.refr[gidx] = ( - bin_meltlimit[gidx, step] - + self.bin_prec[gidx, step] - ) + if (bin_meltlimit[gidx, step] + self.bin_prec[gidx, step]) < self.rf_cold[gidx]: + self.refr[gidx] = bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] # otherwise, refreeze equals the potential refreeze elif self.rf_cold[gidx] > 0: self.refr[gidx] = self.rf_cold[gidx] @@ -794,10 +640,7 @@ def get_annual_mb( self.refr[gidx] = 0 # Track the remaining potential refreeze - self.rf_cold[gidx] -= ( - bin_meltlimit[gidx, step] - + self.bin_prec[gidx, step] - ) + self.rf_cold[gidx] -= bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] # if potential refreeze consumed, set to 0 and set temperature to 0 (temperate firn) if self.rf_cold[gidx] < 0: self.rf_cold[gidx] = 0 @@ -812,13 +655,9 @@ def get_annual_mb( 'Rf_cold remaining:', np.round(self.rf_cold[gidx], 2), 'Snow depth:', - np.round( - self.bin_snowpack[glac_idx_t0[nbin], step], 2 - ), + np.round(self.bin_snowpack[glac_idx_t0[nbin], step], 2), 'Snow melt:', - np.round( - self.bin_meltsnow[glac_idx_t0[nbin], step], 2 - ), + np.round(self.bin_meltsnow[glac_idx_t0[nbin], step], 2), 'Rain:', np.round(self.bin_prec[glac_idx_t0[nbin], step], 2), 'Rfrz:', @@ -831,33 +670,20 @@ def get_annual_mb( # calculate annually and place potential refreeze in user defined month if step % 12 == 0: bin_temp_annual = annualweightedmean_array( - self.bin_temp[ - :, year_start_month_idx:year_stop_month_idx - ], - self.dates_table.iloc[ - year_start_month_idx:year_stop_month_idx, : - ], + self.bin_temp[:, year_start_month_idx:year_stop_month_idx], + self.dates_table.iloc[year_start_month_idx:year_stop_month_idx, :], ) - bin_refreezepotential_annual = ( - -0.69 * bin_temp_annual + 0.0096 - ) / 100 + bin_refreezepotential_annual = (-0.69 * bin_temp_annual + 0.0096) / 100 # Remove negative refreezing values - bin_refreezepotential_annual[ - bin_refreezepotential_annual < 0 - ] = 0 - self.bin_refreezepotential[:, step] = ( - bin_refreezepotential_annual - ) + bin_refreezepotential_annual[bin_refreezepotential_annual < 0] = 0 + self.bin_refreezepotential[:, step] = bin_refreezepotential_annual # Reset refreeze potential every year if self.bin_refreezepotential[:, step].max() > 0: refreeze_potential = self.bin_refreezepotential[:, step] if self.debug_refreeze: print( - 'Year ' - + str(year) - + ' Month ' - + str(self.dates_table.loc[step, 'month']), + 'Year ' + str(year) + ' Month ' + str(self.dates_table.loc[step, 'month']), 'Refreeze potential:', np.round(refreeze_potential[glac_idx_t0[0]], 3), 'Snow depth:', @@ -870,9 +696,7 @@ def get_annual_mb( # Refreeze [m w.e.] # refreeze cannot exceed rain and melt (snow & glacier melt) - self.bin_refreeze[:, step] = ( - self.bin_meltsnow[:, step] + self.bin_prec[:, step] - ) + self.bin_refreeze[:, step] = self.bin_meltsnow[:, step] + self.bin_prec[:, step] # refreeze cannot exceed snow depth self.bin_refreeze[ self.bin_refreeze[:, step] > self.bin_snowpack[:, step], @@ -882,43 +706,28 @@ def get_annual_mb( step, ] # refreeze cannot exceed refreeze potential - self.bin_refreeze[ - self.bin_refreeze[:, step] > refreeze_potential, step - ] = refreeze_potential[ + self.bin_refreeze[self.bin_refreeze[:, step] > refreeze_potential, step] = refreeze_potential[ self.bin_refreeze[:, step] > refreeze_potential ] self.bin_refreeze[ - abs(self.bin_refreeze[:, step]) - < pygem_prms['constants']['tolerance'], + abs(self.bin_refreeze[:, step]) < pygem_prms['constants']['tolerance'], step, ] = 0 # update refreeze potential refreeze_potential -= self.bin_refreeze[:, step] - refreeze_potential[ - abs(refreeze_potential) - < pygem_prms['constants']['tolerance'] - ] = 0 + refreeze_potential[abs(refreeze_potential) < pygem_prms['constants']['tolerance']] = 0 # SNOWPACK REMAINING [m w.e.] - self.snowpack_remaining[:, step] = ( - self.bin_snowpack[:, step] - self.bin_meltsnow[:, step] - ) + self.snowpack_remaining[:, step] = self.bin_snowpack[:, step] - self.bin_meltsnow[:, step] self.snowpack_remaining[ - abs(self.snowpack_remaining[:, step]) - < pygem_prms['constants']['tolerance'], + abs(self.snowpack_remaining[:, step]) < pygem_prms['constants']['tolerance'], step, ] = 0 # Record values - self.glac_bin_melt[glac_idx_t0, step] = self.bin_melt[ - glac_idx_t0, step - ] - self.glac_bin_refreeze[glac_idx_t0, step] = self.bin_refreeze[ - glac_idx_t0, step - ] - self.glac_bin_snowpack[glac_idx_t0, step] = self.bin_snowpack[ - glac_idx_t0, step - ] + self.glac_bin_melt[glac_idx_t0, step] = self.bin_melt[glac_idx_t0, step] + self.glac_bin_refreeze[glac_idx_t0, step] = self.bin_refreeze[glac_idx_t0, step] + self.glac_bin_snowpack[glac_idx_t0, step] = self.bin_snowpack[glac_idx_t0, step] # CLIMATIC MASS BALANCE [m w.e.] self.glac_bin_massbalclim[glac_idx_t0, step] = ( self.bin_acc[glac_idx_t0, step] @@ -929,41 +738,28 @@ def get_annual_mb( # OFF-GLACIER ACCUMULATION, MELT, REFREEZE, AND SNOWPACK if option_areaconstant == False: # precipitation, refreeze, and snowpack are the same both on- and off-glacier - self.offglac_bin_prec[offglac_idx, step] = self.bin_prec[ - offglac_idx, step - ] - self.offglac_bin_refreeze[offglac_idx, step] = ( - self.bin_refreeze[offglac_idx, step] - ) - self.offglac_bin_snowpack[offglac_idx, step] = ( - self.bin_snowpack[offglac_idx, step] - ) + self.offglac_bin_prec[offglac_idx, step] = self.bin_prec[offglac_idx, step] + self.offglac_bin_refreeze[offglac_idx, step] = self.bin_refreeze[offglac_idx, step] + self.offglac_bin_snowpack[offglac_idx, step] = self.bin_snowpack[offglac_idx, step] # Off-glacier melt includes both snow melt and melting of refreezing # (this is not an issue on-glacier because energy remaining melts underlying snow/ice) # melt of refreezing (assumed to be snow) - self.offglac_meltrefreeze = ( - self.surfacetype_ddf_dict[2] * melt_energy_available - ) + self.offglac_meltrefreeze = self.surfacetype_ddf_dict[2] * melt_energy_available # melt of refreezing cannot exceed refreezing - self.offglac_meltrefreeze[ - self.offglac_meltrefreeze > self.bin_refreeze[:, step] - ] = self.bin_refreeze[:, step][ - self.offglac_meltrefreeze > self.bin_refreeze[:, step] - ] + self.offglac_meltrefreeze[self.offglac_meltrefreeze > self.bin_refreeze[:, step]] = ( + self.bin_refreeze[:, step][self.offglac_meltrefreeze > self.bin_refreeze[:, step]] + ) # off-glacier melt = snow melt + refreezing melt self.offglac_bin_melt[offglac_idx, step] = ( - self.bin_meltsnow[offglac_idx, step] - + self.offglac_meltrefreeze[offglac_idx] + self.bin_meltsnow[offglac_idx, step] + self.offglac_meltrefreeze[offglac_idx] ) # ===== RETURN TO ANNUAL LOOP ===== # SURFACE TYPE (-) # Annual climatic mass balance [m w.e.] used to determine the surface type - self.glac_bin_massbalclim_annual[:, year_idx] = ( - self.glac_bin_massbalclim[ - :, year_start_month_idx:year_stop_month_idx - ].sum(1) - ) + self.glac_bin_massbalclim_annual[:, year_idx] = self.glac_bin_massbalclim[ + :, year_start_month_idx:year_stop_month_idx + ].sum(1) # Update surface type for each bin self.surfacetype, firnline_idx = self._surfacetypebinsannual( self.surfacetype, self.glac_bin_massbalclim_annual, year_idx @@ -983,15 +779,9 @@ def get_annual_mb( ) # Mass balance for each bin [m ice per second] - seconds_in_year = ( - self.dayspermonth[year_start_month_idx:year_stop_month_idx].sum() - * 24 - * 3600 - ) + seconds_in_year = self.dayspermonth[year_start_month_idx:year_stop_month_idx].sum() * 24 * 3600 mb = ( - self.glac_bin_massbalclim[:, year_start_month_idx:year_stop_month_idx].sum( - 1 - ) + self.glac_bin_massbalclim[:, year_start_month_idx:year_stop_month_idx].sum(1) * pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice'] / seconds_in_year @@ -1065,10 +855,7 @@ def _convert_glacwide_results( ) # Check annual climatic mass balance (mwea) mb_mwea = ( - glacier_area - * self.glac_bin_massbalclim[ - :, year_start_month_idx:year_stop_month_idx - ].sum(1) + glacier_area * self.glac_bin_massbalclim[:, year_start_month_idx:year_stop_month_idx].sum(1) ).sum() / glacier_area.sum() else: mb_max_loss = 0 @@ -1084,39 +871,30 @@ def _convert_glacwide_results( # Glacier-wide area (m2) self.glac_wide_area_annual[year_idx] = glacier_area.sum() # Glacier-wide volume (m3) - self.glac_wide_volume_annual[year_idx] = ( - section * fls[fl_id].dx_meter - ).sum() + self.glac_wide_volume_annual[year_idx] = (section * fls[fl_id].dx_meter).sum() else: # Glacier-wide area (m2) self.glac_wide_area_annual[year_idx] = glacier_area.sum() # Glacier-wide temperature (degC) self.glac_wide_temp[year_start_month_idx:year_stop_month_idx] = ( - self.bin_temp[:, year_start_month_idx:year_stop_month_idx][glac_idx] - * glacier_area_monthly[glac_idx] + self.bin_temp[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] ).sum(0) / glacier_area.sum() # Glacier-wide precipitation (m3) self.glac_wide_prec[year_start_month_idx:year_stop_month_idx] = ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][glac_idx] - * glacier_area_monthly[glac_idx] + self.bin_prec[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] ).sum(0) # Glacier-wide accumulation (m3 w.e.) self.glac_wide_acc[year_start_month_idx:year_stop_month_idx] = ( - self.bin_acc[:, year_start_month_idx:year_stop_month_idx][glac_idx] - * glacier_area_monthly[glac_idx] + self.bin_acc[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] ).sum(0) # Glacier-wide refreeze (m3 w.e.) self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx] = ( - self.glac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][ - glac_idx - ] + self.glac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] ).sum(0) # Glacier-wide melt (m3 w.e.) self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.glac_bin_melt[:, year_start_month_idx:year_stop_month_idx][ - glac_idx - ] + self.glac_bin_melt[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] ).sum(0) # Glacier-wide total mass balance (m3 w.e.) @@ -1124,30 +902,23 @@ def _convert_glacwide_results( self.glac_wide_acc[year_start_month_idx:year_stop_month_idx] + self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx] - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] - - self.glac_wide_frontalablation[ - year_start_month_idx:year_stop_month_idx - ] + - self.glac_wide_frontalablation[year_start_month_idx:year_stop_month_idx] ) # If mass loss more negative than glacier mass, reduce melt so glacier completely melts (no excess) if icethickness_t0 is not None and mb_mwea < mb_max_loss: - melt_yr_raw = self.glac_wide_melt[ - year_start_month_idx:year_stop_month_idx - ].sum() + melt_yr_raw = self.glac_wide_melt[year_start_month_idx:year_stop_month_idx].sum() melt_yr_max = ( self.glac_wide_volume_annual[year_idx] * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] + self.glac_wide_acc[year_start_month_idx:year_stop_month_idx].sum() - + self.glac_wide_refreeze[ - year_start_month_idx:year_stop_month_idx - ].sum() + + self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx].sum() ) melt_frac = melt_yr_max / melt_yr_raw # Update glacier-wide melt (m3 w.e.) self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] - * melt_frac + self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] * melt_frac ) # Glacier-wide runoff (m3) @@ -1159,9 +930,7 @@ def _convert_glacwide_results( # Snow line altitude (m a.s.l.) heights_monthly = heights[:, np.newaxis].repeat(12, axis=1) snow_mask = np.zeros(heights_monthly.shape) - snow_mask[ - self.glac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx] > 0 - ] = 1 + snow_mask[self.glac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx] > 0] = 1 heights_monthly_wsnow = heights_monthly * snow_mask heights_monthly_wsnow[heights_monthly_wsnow == 0] = np.nan heights_change = np.zeros(heights.shape) @@ -1176,19 +945,13 @@ def _convert_glacwide_results( snowline_idx_nan = [] for ncol in range(heights_monthly_wsnow.shape[1]): if ~np.isnan(heights_monthly_wsnow[:, ncol]).all(): - snowline_idx[ncol] = np.nanargmin( - heights_monthly_wsnow[:, ncol] - ) + snowline_idx[ncol] = np.nanargmin(heights_monthly_wsnow[:, ncol]) else: snowline_idx_nan.append(ncol) - heights_manual = ( - heights[snowline_idx] - heights_change[snowline_idx] / 2 - ) + heights_manual = heights[snowline_idx] - heights_change[snowline_idx] / 2 heights_manual[snowline_idx_nan] = np.nan # this line below causes a potential All-NaN slice encountered issue at some time steps - self.glac_wide_snowline[year_start_month_idx:year_stop_month_idx] = ( - heights_manual - ) + self.glac_wide_snowline[year_start_month_idx:year_stop_month_idx] = heights_manual # Equilibrium line altitude (m a.s.l.) ela_mask = np.zeros(heights.shape) @@ -1199,16 +962,12 @@ def _convert_glacwide_results( self.glac_wide_ELA_annual[year_idx] = np.nan else: ela_idx = np.nanargmin(ela_onlypos) - self.glac_wide_ELA_annual[year_idx] = ( - heights[ela_idx] - heights_change[ela_idx] / 2 - ) + self.glac_wide_ELA_annual[year_idx] = heights[ela_idx] - heights_change[ela_idx] / 2 # ===== Off-glacier ==== offglac_idx = np.where(self.offglac_bin_area_annual[:, year_idx] > 0)[0] if option_areaconstant == False and len(offglac_idx) > 0: - offglacier_area_monthly = self.offglac_bin_area_annual[:, year_idx][ - :, np.newaxis - ].repeat(12, axis=1) + offglacier_area_monthly = self.offglac_bin_area_annual[:, year_idx][:, np.newaxis].repeat(12, axis=1) # Off-glacier precipitation (m3) self.offglac_wide_prec[year_start_month_idx:year_stop_month_idx] = ( @@ -1217,16 +976,12 @@ def _convert_glacwide_results( ).sum(0) # Off-glacier melt (m3 w.e.) self.offglac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_melt[:, year_start_month_idx:year_stop_month_idx][ - offglac_idx - ] + self.offglac_bin_melt[:, year_start_month_idx:year_stop_month_idx][offglac_idx] * offglacier_area_monthly[offglac_idx] ).sum(0) # Off-glacier refreeze (m3 w.e.) self.offglac_wide_refreeze[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][ - offglac_idx - ] + self.offglac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][offglac_idx] * offglacier_area_monthly[offglac_idx] ).sum(0) # Off-glacier runoff (m3) @@ -1237,9 +992,7 @@ def _convert_glacwide_results( ) # Off-glacier snowpack (m3 w.e.) self.offglac_wide_snowpack[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx][ - offglac_idx - ] + self.offglac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx][offglac_idx] * offglacier_area_monthly[offglac_idx] ).sum(0) @@ -1281,32 +1034,21 @@ def ensure_mass_conservation(self, diag): chg_idx_melt = list(set(chg_idx).intersection(chg_idx_posmbmod)) vol_change_annual_melt_reduction[chg_idx_melt] = ( - 1 - - vol_change_annual_dif[chg_idx_melt] - / vol_change_annual_mbmod_melt[chg_idx_melt] + 1 - vol_change_annual_dif[chg_idx_melt] / vol_change_annual_mbmod_melt[chg_idx_melt] ) - vol_change_annual_melt_reduction_monthly = np.repeat( - vol_change_annual_melt_reduction, 12 - ) + vol_change_annual_melt_reduction_monthly = np.repeat(vol_change_annual_melt_reduction, 12) # Glacier-wide melt (m3 w.e.) - self.glac_wide_melt = ( - self.glac_wide_melt * vol_change_annual_melt_reduction_monthly - ) + self.glac_wide_melt = self.glac_wide_melt * vol_change_annual_melt_reduction_monthly # Glacier-wide total mass balance (m3 w.e.) self.glac_wide_massbaltotal = ( - self.glac_wide_acc - + self.glac_wide_refreeze - - self.glac_wide_melt - - self.glac_wide_frontalablation + self.glac_wide_acc + self.glac_wide_refreeze - self.glac_wide_melt - self.glac_wide_frontalablation ) # Glacier-wide runoff (m3) - self.glac_wide_runoff = ( - self.glac_wide_prec + self.glac_wide_melt - self.glac_wide_refreeze - ) + self.glac_wide_runoff = self.glac_wide_prec + self.glac_wide_melt - self.glac_wide_refreeze self.glac_wide_volume_change_ignored_annual = vol_change_annual_dif @@ -1342,24 +1084,12 @@ def _surfacetypebinsinitial(self, elev_bins): surfacetype = np.zeros(self.glacier_area_initial.shape) # Option 1 - initial surface type based on the median elevation if pygem_prms['mb']['option_surfacetype_initial'] == 1: - surfacetype[ - (elev_bins < self.glacier_rgi_table.loc['Zmed']) - & (self.glacier_area_initial > 0) - ] = 1 - surfacetype[ - (elev_bins >= self.glacier_rgi_table.loc['Zmed']) - & (self.glacier_area_initial > 0) - ] = 2 + surfacetype[(elev_bins < self.glacier_rgi_table.loc['Zmed']) & (self.glacier_area_initial > 0)] = 1 + surfacetype[(elev_bins >= self.glacier_rgi_table.loc['Zmed']) & (self.glacier_area_initial > 0)] = 2 # Option 2 - initial surface type based on the mean elevation elif pygem_prms['mb']['option_surfacetype_initial'] == 2: - surfacetype[ - (elev_bins < self.glacier_rgi_table['Zmean']) - & (self.glacier_area_initial > 0) - ] = 1 - surfacetype[ - (elev_bins >= self.glacier_rgi_table['Zmean']) - & (self.glacier_area_initial > 0) - ] = 2 + surfacetype[(elev_bins < self.glacier_rgi_table['Zmean']) & (self.glacier_area_initial > 0)] = 1 + surfacetype[(elev_bins >= self.glacier_rgi_table['Zmean']) & (self.glacier_area_initial > 0)] = 2 else: print( "This option for 'option_surfacetype' does not exist. Please choose an option that exists. " @@ -1380,9 +1110,7 @@ def _surfacetypebinsinitial(self, elev_bins): # snow on the surface anywhere. return surfacetype, firnline_idx - def _surfacetypebinsannual( - self, surfacetype, glac_bin_massbalclim_annual, year_idx - ): + def _surfacetypebinsannual(self, surfacetype, glac_bin_massbalclim_annual, year_idx): """ Update surface type according to climatic mass balance over the last five years. @@ -1426,13 +1154,9 @@ def _surfacetypebinsannual( # less than 5 years, then use the average of the existing years. if year_idx < 5: # Calculate average annual climatic mass balance since run began - massbal_clim_mwe_runningavg = glac_bin_massbalclim_annual[ - :, 0 : year_idx + 1 - ].mean(1) + massbal_clim_mwe_runningavg = glac_bin_massbalclim_annual[:, 0 : year_idx + 1].mean(1) else: - massbal_clim_mwe_runningavg = glac_bin_massbalclim_annual[ - :, year_idx - 4 : year_idx + 1 - ].mean(1) + massbal_clim_mwe_runningavg = glac_bin_massbalclim_annual[:, year_idx - 4 : year_idx + 1].mean(1) # If the average annual specific climatic mass balance is negative, then the surface type is ice (or debris) surfacetype[(surfacetype != 0) & (massbal_clim_mwe_runningavg <= 0)] = 1 # If the average annual specific climatic mass balance is positive, then the surface type is snow (or firn) @@ -1489,9 +1213,7 @@ def _surfacetypeDDFdict( if option_ddf_firn == 0: surfacetype_ddf_dict[3] = modelprms['ddfsnow'] elif option_ddf_firn == 1: - surfacetype_ddf_dict[3] = np.mean( - [modelprms['ddfsnow'], modelprms['ddfice']] - ) + surfacetype_ddf_dict[3] = np.mean([modelprms['ddfsnow'], modelprms['ddfice']]) return surfacetype_ddf_dict diff --git a/pygem/mcmc.py b/pygem/mcmc.py index 04a7b0f2..2b6df0e8 100644 --- a/pygem/mcmc.py +++ b/pygem/mcmc.py @@ -1,7 +1,7 @@ """ Python Glacier Evolution Model (PyGEM) -copyright © 2018 David Rounce , David Rounce Distributed under the MIT license @@ -38,33 +38,38 @@ def inverse_z_normalize(z_params, means, std_devs): return z_params * std_devs + means -def log_normal_density(x, **kwargs): +def log_normal_density(x, method='mean', **kwargs): """ - Computes the log probability density of a normal distribution. + Evaluate the log probability density of a normal distribution. Parameters: - - x: Input tensor where you want to evaluate the log probability. - - mu: Mean of the normal distribution. - - sigma: Standard deviation of the normal distribution. + - x: input data point or array of data points. + - mu: mean of the normal distribution (diagonal covariance matrix). + - sigma: standard deviation (diagonal elements of the covariance matrix). Returns: - Log probability density at the given input tensor x. + log probability density """ mu, sigma = kwargs['mu'], kwargs['sigma'] - # flatten arrays and get dimensionality - x = x.flatten() - mu = mu.flatten() - sigma = sigma.flatten() + # ensure tensors are flattened + x, mu, sigma = map(torch.flatten, (x, mu, sigma)) + + # compute log normal density per element k = mu.shape[-1] - return torch.tensor( - [ - -k / 2.0 * torch.log(torch.tensor(2 * np.pi)) - - torch.log(sigma).nansum() - - 0.5 * (((x - mu) / sigma) ** 2).nansum() - ] - ) + # scale sigma by sqrt(k) + # sigma *= torch.sqrt(torch.tensor(k)) + + # compute log normal density per element + log_prob = -k / 2.0 * torch.log(torch.tensor(2 * np.pi)) - torch.log(sigma) - 0.5 * ((x - mu) / sigma) ** 2 + + if method == 'sum': + return torch.tensor([log_prob.nansum()]) + elif method == 'mean': + return torch.tensor([log_prob.nanmean()]) + else: + raise ValueError("method must be one of ['sum', 'mean']") def log_gamma_density(x, **kwargs): @@ -80,15 +85,10 @@ def log_gamma_density(x, **kwargs): Log probability density at the given input tensor x. """ alpha, beta = kwargs['alpha'], kwargs['beta'] # shape, scale - return ( - alpha * torch.log(beta) - + (alpha - 1) * torch.log(x) - - beta * x - - torch.lgamma(alpha) - ) + return alpha * torch.log(beta) + (alpha - 1) * torch.log(x) - beta * x - torch.lgamma(alpha) -def log_truncated_normal(x, **kwargs): +def log_truncated_normal_density(x, **kwargs): """ Computes the log probability density of a truncated normal distribution. @@ -120,19 +120,37 @@ def log_truncated_normal(x, **kwargs): return torch.log(pdf) - torch.log(normalization) +def log_uniform_density(x, **kwargs): + """ + Computes the log probability density of a Uniform distribution for scalar x. + + Parameters: + - x: Scalar tensor where you want to evaluate the log probability. + - low: Lower bound of the uniform distribution. + - high: Upper bound of the uniform distribution. + + Returns: + Scalar log probability density at x. + """ + low, high = kwargs['low'], kwargs['high'] + if low <= x <= high: + return -torch.log(high - low) + else: + return torch.tensor([float('-inf')]) + + # mapper dictionary - maps to appropriate log probability density function for given distribution `type` log_prob_fxn_map = { 'normal': log_normal_density, 'gamma': log_gamma_density, - 'truncnormal': log_truncated_normal, + 'truncnormal': log_truncated_normal_density, + 'uniform': log_uniform_density, } # mass balance posterior class class mbPosterior: - def __init__( - self, obs, priors, mb_func, mb_args=None, potential_fxns=None, **kwargs - ): + def __init__(self, obs, priors, mb_func, mb_args=None, potential_fxns=None, **kwargs): # obs will be passed as a list, where each item is a tuple with the first element being the mean observation, and the second being the variance self.obs = obs self.priors = copy.deepcopy(priors) @@ -142,6 +160,11 @@ def __init__( self.preds = None self.check_priors() + self.ela = kwargs.get('ela', None) + self.bin_z = kwargs.get('bin_z', None) + if self.ela: + self.abl_mask = self.bin_z < self.ela + # get mean and std for each parameter type self.means = torch.tensor([params['mu'] for params in self.priors.values()]) self.stds = torch.tensor([params['sigma'] for params in self.priors.values()]) @@ -165,20 +188,20 @@ def check_priors(self): for k in self.priors.keys(): if self.priors[k]['type'] == 'gamma' and 'mu' not in self.priors[k].keys(): self.priors[k]['mu'] = self.priors[k]['alpha'] / self.priors[k]['beta'] - self.priors[k]['sigma'] = float( - np.sqrt(self.priors[k]['alpha']) / self.priors[k]['beta'] - ) + self.priors[k]['sigma'] = float(np.sqrt(self.priors[k]['alpha']) / self.priors[k]['beta']) + + if self.priors[k]['type'] == 'uniform' and 'mu' not in self.priors[k].keys(): + self.priors[k]['mu'] = (self.priors[k]['low'] / self.priors[k]['high']) / 2 + self.priors[k]['sigma'] = (self.priors[k]['high'] - self.priors[k]['low']) / (12 ** (1 / 2)) # update modelprms for evaluation def update_modelprms(self, m): for i, k in enumerate(['tbias', 'kp', 'ddfsnow']): self.mb_args[1][k] = float(m[i]) - self.mb_args[1]['ddfice'] = ( - self.mb_args[1]['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] - ) + self.mb_args[1]['ddfice'] = self.mb_args[1]['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] # get mb_pred - def get_mb_pred(self, m): + def get_model_pred(self, m): if self.mb_args: self.update_modelprms(m) self.preds = self.mb_func(*self.mb_args) @@ -186,9 +209,7 @@ def get_mb_pred(self, m): self.preds = self.mb_func([*m]) if not isinstance(self.preds, tuple): self.preds = [self.preds] - self.preds = [ - torch.tensor(item) for item in self.preds - ] # make all preds torch.tensor() objects + self.preds = [torch.tensor(item) for item in self.preds] # make all preds torch.tensor() objects # get total log prior density def log_prior(self, m): @@ -202,28 +223,65 @@ def log_prior(self, m): return log_prior # get log likelihood - def log_likelihood(self): + def log_likelihood(self, m): log_likehood = 0 for i, pred in enumerate(self.preds): - log_likehood += log_normal_density( - self.obs[i][0], **{'mu': pred, 'sigma': self.obs[i][1]} - ) + # --- Check for invalid predictions early --- + if torch.all(pred == float('-inf')): + # Invalid model output -> assign -inf likelihood + return torch.tensor([-float('inf')]) + + if i == 0: + # --- Base case: mass balance likelihood --- + log_likehood += log_normal_density( + self.obs[i][0], # observed values + mu=pred, # predicted values + sigma=self.obs[i][1], # observation uncertainty + ) + + elif i == 1 and len(m) > 3: + # --- Extended case: apply density scaling to get binned elevation change --- + # Create density field, separate values for ablation/accumulation zones + rho = np.ones_like(self.bin_z) + rho[self.abl_mask] = m[3] # rhoabl + rho[~self.abl_mask] = m[4] # rhoacc + rho = torch.tensor(rho) + self.preds[i] = pred = ( + self.preds[i] * rho[:, np.newaxis] / pygem_prms['constants']['density_ice'] + ) # scale prediction by model density values (convert from m ice to m thickness change) + + log_likehood += log_normal_density( + self.obs[i][0], # observations + mu=pred, # scaled predictions + sigma=self.obs[i][1], # uncertainty + ) return log_likehood - # get log potential (sum up as any declared potential functions) + # compute the log-potential, summing over all declared potential functions. def log_potential(self, m): - log_potential = 0 - for potential_function in self.potential_functions: - log_potential += potential_function(*m, **{'massbal': self.preds[0]}) - return log_potential + # --- Base arguments --- + # kp, tbias, ddfsnow, massbal + kwargs = { + 'kp': m[0], + 'tbias': m[1], + 'ddfsnow': m[2], + 'massbal': self.preds[0], + } + + # --- Optional arguments(if len(m) > 3) --- + # rhoabl, rhoacc + if len(m) > 3: + kwargs['rhoabl'] = m[-2] + kwargs['rhoacc'] = m[-1] + + # --- Evaluate all potential functions --- + return sum(pf(**kwargs) for pf in self.potential_functions) # get log posterior (sum of log prior, log likelihood and log potential) def log_posterior(self, m): # anytime log_posterior is called for a new step, calculate the predicted mass balance - self.get_mb_pred(m) - return self.log_prior(m) + self.log_likelihood() + self.log_potential( - m - ), self.preds + self.get_model_pred(m) + return self.log_prior(m) + self.log_likelihood(m) + self.log_potential(m), self.preds # Metropolis-Hastings Markov chain Monte Carlo class @@ -250,11 +308,13 @@ def get_n_rm(self, tol=0.1): """ n_params = len(self.m_chain[0]) n_rms = [] + # get z-normalized vals + z_norms = [z_normalize(vals, self.means, self.stds) for vals in self.m_chain] for i in range(n_params): - vals = [val[i] for val in self.m_chain] - first_value = vals[0] + param_vals = [vals[i] for vals in z_norms] + first_value = param_vals[0] count = 0 - for value in vals: + for value in param_vals: if abs(value - first_value) <= tol: count += 1 else: @@ -289,7 +349,7 @@ def sample( progress_bar=False, ): # Compute initial unscaled log-posterior - P_0, pred_0 = log_posterior(inverse_z_normalize(m_0, self.means, self.stds)) + P_0, pred_0 = log_posterior(m_0) n = len(m_0) @@ -302,12 +362,13 @@ def sample( # Propose new value according to # proposal distribution Q(m) = N(m_0,h) step = torch.randn(n) * h - m_prime = m_0 + step + + # update m_prime based on normalized values + m_prime = z_normalize(m_0, self.means, self.stds) + step + m_prime = inverse_z_normalize(m_prime, self.means, self.stds) # Compute new unscaled log-posterior - P_1, pred_1 = log_posterior( - inverse_z_normalize(m_prime, self.means, self.stds) - ) + P_1, pred_1 = log_posterior(m_prime) # Compute logarithm of probability ratio log_ratio = P_1 - P_0 @@ -332,9 +393,7 @@ def sample( self.P_chain.append(P_0) self.m_chain.append(m_0) self.m_primes.append(m_prime) - self.acceptance.append( - self.naccept / (i + (thin_factor * self.n_rm)) - ) + self.acceptance.append(self.naccept / (i + (thin_factor * self.n_rm))) for j in range(len(pred_1)): if j not in self.preds_chain.keys(): self.preds_chain[j] = [] @@ -370,220 +429,3 @@ def sample( torch.vstack(self.steps), self.acceptance, ) - - -### some other useful functions ### - - -def effective_n(x): - """ - Compute the effective sample size of a trace. - - Takes the trace and computes the effective sample size - according to its detrended autocorrelation. - - Parameters - ---------- - x : list or array of chain samples - - Returns - ------- - effective_n : int - effective sample size - """ - if len(set(x)) == 1: - return 1 - try: - # detrend trace using mean to be consistent with statistics - # definition of autocorrelation - x = np.asarray(x) - x = x - x.mean() - # compute autocorrelation (note: only need second half since - # they are symmetric) - rho = np.correlate(x, x, mode='full') - rho = rho[len(rho) // 2 :] - # normalize the autocorrelation values - # note: rho[0] is the variance * n_samples, so this is consistent - # with the statistics definition of autocorrelation on wikipedia - # (dividing by n_samples gives you the expected value). - rho_norm = rho / rho[0] - # Iterate until sum of consecutive estimates of autocorrelation is - # negative to avoid issues with the sum being -0.5, which returns an - # effective_n of infinity - negative_autocorr = False - t = 1 - n = len(x) - while not negative_autocorr and (t < n): - if not t % 2: - negative_autocorr = sum(rho_norm[t - 1 : t + 1]) < 0 - t += 1 - return int(n / (1 + 2 * rho_norm[1:t].sum())) - except: - return None - - -def plot_chain( - m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show=False, fpath=None -): - # Plot the trace of the parameters - fig, axes = plt.subplots(5, 1, figsize=(6, 8), sharex=True) - m_chain = m_chain.detach().numpy() - m_primes = m_primes.detach().numpy() - - # get n_eff - neff = [effective_n(arr) for arr in m_chain.T] - - axes[0].plot( - [], - [], - label=f'mean={np.mean(m_chain[:, 0]):.3f}\nstd={np.std(m_chain[:, 0]):.3f}', - ) - l0 = axes[0].legend( - loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize - ) - - axes[0].plot(m_primes[:, 0], '.', ms=ms, label='proposed', c='tab:blue') - axes[0].plot(m_chain[:, 0], '.', ms=ms, label='accepted', c='tab:orange') - hands, ls = axes[0].get_legend_handles_labels() - - # axes[0].add_artist(leg) - axes[0].set_ylabel(r'$T_{bias}$', fontsize=fontsize) - - axes[1].plot(m_primes[:, 1], '.', ms=ms, c='tab:blue') - axes[1].plot(m_chain[:, 1], '.', ms=ms, c='tab:orange') - axes[1].plot( - [], - [], - label=f'mean={np.mean(m_chain[:, 1]):.3f}\nstd={np.std(m_chain[:, 1]):.3f}', - ) - l1 = axes[1].legend( - loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize - ) - axes[1].set_ylabel(r'$K_p$', fontsize=fontsize) - - axes[2].plot(m_primes[:, 2], '.', ms=ms, c='tab:blue') - axes[2].plot(m_chain[:, 2], '.', ms=ms, c='tab:orange') - axes[2].plot( - [], - [], - label=f'mean={np.mean(m_chain[:, 2]):.3f}\nstd={np.std(m_chain[:, 2]):.3f}', - ) - l2 = axes[2].legend( - loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize - ) - axes[2].set_ylabel(r'$fsnow$', fontsize=fontsize) - - axes[3].fill_between( - np.arange(len(ar)), - mb_obs[0] - (2 * mb_obs[1]), - mb_obs[0] + (2 * mb_obs[1]), - color='grey', - alpha=0.3, - ) - axes[3].fill_between( - np.arange(len(ar)), - mb_obs[0] - mb_obs[1], - mb_obs[0] + mb_obs[1], - color='grey', - alpha=0.3, - ) - axes[3].plot(m_primes[:, 3], '.', ms=ms, c='tab:blue') - axes[3].plot(m_chain[:, 3], '.', ms=ms, c='tab:orange') - axes[3].plot( - [], - [], - label=f'mean={np.mean(m_chain[:, 3]):.3f}\nstd={np.std(m_chain[:, 3]):.3f}', - ) - l3 = axes[3].legend( - loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize - ) - axes[3].set_ylabel(r'$\dot{{b}}$', fontsize=fontsize) - - axes[4].plot(ar, 'tab:orange', lw=1) - axes[4].plot( - np.convolve(ar, np.ones(100) / 100, mode='valid'), - 'k', - label='moving avg.', - lw=1, - ) - l4 = axes[4].legend( - loc='upper left', handlelength=0.5, borderaxespad=0, fontsize=fontsize - ) - axes[4].set_ylabel(r'$AR$', fontsize=fontsize) - - for i, ax in enumerate(axes): - ax.xaxis.set_ticks_position('both') - ax.yaxis.set_ticks_position('both') - ax.tick_params(axis='both', direction='inout') - if i == 4: - continue - ax.plot([], [], label=f'n_eff={neff[i]}') - hands, ls = ax.get_legend_handles_labels() - if i == 0: - ax.legend( - handles=[hands[1], hands[2], hands[3]], - labels=[ls[1], ls[2], ls[3]], - loc='upper left', - borderaxespad=0, - handlelength=0, - fontsize=fontsize, - ) - else: - ax.legend( - handles=[hands[-1]], - labels=[ls[-1]], - loc='upper left', - borderaxespad=0, - handlelength=0, - fontsize=fontsize, - ) - - axes[0].add_artist(l0) - axes[1].add_artist(l1) - axes[2].add_artist(l2) - axes[3].add_artist(l3) - axes[4].add_artist(l4) - axes[0].set_xlim([0, m_chain.shape[0]]) - axes[0].set_title(title, fontsize=fontsize) - plt.tight_layout() - plt.subplots_adjust(hspace=0.1, wspace=0) - if fpath: - fig.savefig(fpath, dpi=400) - if show: - plt.show(block=True) # wait until the figure is closed - plt.close(fig) - return - - -def plot_resid_hist(obs, preds, title, fontsize=8, show=False, fpath=None): - # Plot the trace of the parameters - fig, axes = plt.subplots(1, 1, figsize=(3, 2)) - # subtract obs from preds to get residuals - diffs = np.concatenate( - [pred.flatten() - obs[0].flatten().numpy() for pred in preds] - ) - # mask nans to avoid error in np.histogram() - diffs = diffs[~np.isnan(diffs)] - # Calculate histogram counts and bin edges - counts, bin_edges = np.histogram(diffs, bins=20) - pct = counts / counts.sum() * 100 - bin_width = bin_edges[1] - bin_edges[0] - axes.bar( - bin_edges[:-1], - pct, - width=bin_width, - edgecolor='black', - color='gray', - align='edge', - ) - axes.set_xlabel('residuals (pred - obs)', fontsize=fontsize) - axes.set_ylabel('count (%)', fontsize=fontsize) - axes.set_title(title, fontsize=fontsize) - plt.tight_layout() - plt.subplots_adjust(hspace=0.1, wspace=0) - if fpath: - fig.savefig(fpath, dpi=400) - if show: - plt.show(block=True) # wait until the figure is closed - plt.close(fig) - return diff --git a/pygem/oggm_compat.py b/pygem/oggm_compat.py index e7c95a3c..7c08d4ee 100755 --- a/pygem/oggm_compat.py +++ b/pygem/oggm_compat.py @@ -23,9 +23,13 @@ from oggm.core.massbalance import MassBalanceModel from pygem.setup.config import ConfigManager - -# from oggm.shop import rgitopo -from pygem.shop import debris, icethickness, mbdata, meltextent_and_snowline_1d +from pygem.shop import ( + debris, + elevchange1d, + icethickness, + mbdata, + meltextent_and_snowline_1d, +) # instantiate ConfigManager config_manager = ConfigManager() @@ -122,21 +126,18 @@ def single_flowline_glacier_directory( if not os.path.isfile(gdir.get_filepath('mb_calib_pygem')): workflow.execute_entity_task(mbdata.mb_df_to_gdir, gdir) # debris thickness and melt enhancement factors - if not os.path.isfile(gdir.get_filepath('debris_ed')) or not os.path.isfile( - gdir.get_filepath('debris_hd') - ): + if not os.path.isfile(gdir.get_filepath('debris_ed')) or not os.path.isfile(gdir.get_filepath('debris_hd')): workflow.execute_entity_task(debris.debris_to_gdir, gdir) workflow.execute_entity_task(debris.debris_binned, gdir) + # 1d elevation change calibration data + if not os.path.isfile(gdir.get_filepath('elev_change_1d')): + workflow.execute_entity_task(elevchange1d.elev_change_1d_to_gdir, gdir) # 1d melt extent calibration data if not os.path.isfile(gdir.get_filepath('meltextent_1d')): - workflow.execute_entity_task( - meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir - ) + workflow.execute_entity_task(meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir) # 1d snowline calibration data if not os.path.isfile(gdir.get_filepath('snowline_1d')): - workflow.execute_entity_task( - meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir - ) + workflow.execute_entity_task(meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir) return gdir @@ -230,19 +231,16 @@ def single_flowline_glacier_directory_with_calving( # mass balance calibration data (note facorrected kwarg) if not os.path.isfile(gdir.get_filepath('mb_calib_pygem')): - workflow.execute_entity_task( - mbdata.mb_df_to_gdir, gdir, **{'facorrected': facorrected} - ) + workflow.execute_entity_task(mbdata.mb_df_to_gdir, gdir, **{'facorrected': facorrected}) + # 1d elevation change calibration data + if not os.path.isfile(gdir.get_filepath('elev_change_1d')): + workflow.execute_entity_task(elevchange1d.elev_change_1d_to_gdir, gdir) # 1d melt extent calibration data if not os.path.isfile(gdir.get_filepath('meltextent_1d')): - workflow.execute_entity_task( - meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir - ) + workflow.execute_entity_task(meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir) # 1d snowline calibration data if not os.path.isfile(gdir.get_filepath('snowline_1d')): - workflow.execute_entity_task( - meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir - ) + workflow.execute_entity_task(meltextent_and_snowline_1d.snowline_1d_to_gdir, gdir) return gdir @@ -262,23 +260,15 @@ def get_spinup_flowlines(gdir, y0=None): flowline object """ # instantiate flowline.FileModel object from model_geometry_dynamic_spinup - fmd_dynamic = FileModel( - gdir.get_filepath('model_geometry', filesuffix='_dynamic_spinup_pygem_mb') - ) + fmd_dynamic = FileModel(gdir.get_filepath('model_geometry', filesuffix='_dynamic_spinup_pygem_mb')) # run FileModel to startyear (it will be initialized at `spinup_start_yr`) fmd_dynamic.run_until(y0) # write flowlines - gdir.write_pickle( - fmd_dynamic.fls, 'model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}' - ) + gdir.write_pickle(fmd_dynamic.fls, 'model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}') # add debris - debris.debris_binned( - gdir, fl_str='model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}' - ) + debris.debris_binned(gdir, fl_str='model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}') # return flowlines - return gdir.read_pickle( - 'model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}' - ) + return gdir.read_pickle('model_flowlines', filesuffix=f'_dynamic_spinup_pygem_mb_{y0}') def update_cfg(updates, dict_name='PARAMS'): @@ -295,11 +285,7 @@ def update_cfg(updates, dict_name='PARAMS'): try: target_dict = getattr(cfg, dict_name) for key, subdict in updates.items(): - if ( - key in target_dict - and isinstance(target_dict[key], dict) - and isinstance(subdict, dict) - ): + if key in target_dict and isinstance(target_dict[key], dict) and isinstance(subdict, dict): for subkey, value in subdict.items(): if subkey in cfg[dict][key]: target_dict[key][subkey] = value diff --git a/pygem/output.py b/pygem/output.py index 47f880eb..c76005f2 100644 --- a/pygem/output.py +++ b/pygem/output.py @@ -132,7 +132,9 @@ def set_fn(self, outfn=None): if self.option_calibration: self.outfn += f'{self.option_calibration}_' else: - self.outfn += f'kp{self.modelprms["kp"]}_ddfsnow{self.modelprms["ddfsnow"]}_tbias{self.modelprms["tbias"]}_' + self.outfn += ( + f'kp{self.modelprms["kp"]}_ddfsnow{self.modelprms["ddfsnow"]}_tbias{self.modelprms["tbias"]}_' + ) if self.sim_climate_name not in ['ERA-Interim', 'ERA5', 'COAWST']: self.outfn += f'ba{self.option_bias_adjustment}_' else: @@ -171,20 +173,14 @@ def _set_time_vals(self): ] elif pygem_prms['climate']['sim_wateryear'] == 'calendar': self.year_type = 'calendar year' - self.annual_columns = np.unique(self.dates_table['year'].values)[ - 0 : int(self.dates_table.shape[0] / 12) - ] + self.annual_columns = np.unique(self.dates_table['year'].values)[0 : int(self.dates_table.shape[0] / 12)] elif pygem_prms['climate']['sim_wateryear'] == 'custom': self.year_type = 'custom year' self.time_values = self.dates_table['date'].values.tolist() - self.time_values = [ - cftime.DatetimeNoLeap(x.year, x.month, x.day) for x in self.time_values - ] + self.time_values = [cftime.DatetimeNoLeap(x.year, x.month, x.day) for x in self.time_values] # append additional year to self.year_values to account for mass and area at end of period self.year_values = self.annual_columns - self.year_values = np.concatenate( - (self.year_values, np.array([self.annual_columns[-1] + 1])) - ) + self.year_values = np.concatenate((self.year_values, np.array([self.annual_columns[-1] + 1]))) def _model_params_record(self): """Build model parameters attribute dictionary to be saved to output dataset.""" @@ -212,24 +208,12 @@ def _update_modelparams_record(self): def _init_dicts(self): """Initialize output coordinate and attribute dictionaries.""" self.output_coords_dict = collections.OrderedDict() - self.output_coords_dict['RGIId'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) - self.output_coords_dict['CenLon'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) - self.output_coords_dict['CenLat'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) - self.output_coords_dict['O1Region'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) - self.output_coords_dict['O2Region'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) - self.output_coords_dict['Area'] = collections.OrderedDict( - [('glac', self.glac_values)] - ) + self.output_coords_dict['RGIId'] = collections.OrderedDict([('glac', self.glac_values)]) + self.output_coords_dict['CenLon'] = collections.OrderedDict([('glac', self.glac_values)]) + self.output_coords_dict['CenLat'] = collections.OrderedDict([('glac', self.glac_values)]) + self.output_coords_dict['O1Region'] = collections.OrderedDict([('glac', self.glac_values)]) + self.output_coords_dict['O2Region'] = collections.OrderedDict([('glac', self.glac_values)]) + self.output_coords_dict['Area'] = collections.OrderedDict([('glac', self.glac_values)]) self.output_attrs_dict = { 'time': { 'long_name': 'time', @@ -282,10 +266,7 @@ def create_xr_ds(self): for vn in self.output_coords_dict.keys(): count_vn += 1 empty_holder = np.zeros( - [ - len(self.output_coords_dict[vn][i]) - for i in list(self.output_coords_dict[vn].keys()) - ] + [len(self.output_coords_dict[vn][i]) for i in list(self.output_coords_dict[vn].keys())] ) output_xr_ds_ = xr.Dataset( {vn: (list(self.output_coords_dict[vn].keys()), empty_holder)}, @@ -307,17 +288,11 @@ def create_xr_ds(self): if vn not in noencoding_vn: self.encoding[vn] = {'_FillValue': None, 'zlib': True, 'complevel': 9} - self.output_xr_ds['RGIId'].values = np.array( - [self.glacier_rgi_table.loc['RGIId']] - ) + self.output_xr_ds['RGIId'].values = np.array([self.glacier_rgi_table.loc['RGIId']]) self.output_xr_ds['CenLon'].values = np.array([self.glacier_rgi_table.CenLon]) self.output_xr_ds['CenLat'].values = np.array([self.glacier_rgi_table.CenLat]) - self.output_xr_ds['O1Region'].values = np.array( - [self.glacier_rgi_table.O1Region] - ) - self.output_xr_ds['O2Region'].values = np.array( - [self.glacier_rgi_table.O2Region] - ) + self.output_xr_ds['O1Region'].values = np.array([self.glacier_rgi_table.O1Region]) + self.output_xr_ds['O2Region'].values = np.array([self.glacier_rgi_table.O2Region]) self.output_xr_ds['Area'].values = np.array([self.glacier_rgi_table.Area * 1e6]) self.output_xr_ds.attrs = { @@ -431,10 +406,8 @@ def _update_dicts(self): # if nsims > 1, store median-absolute deviation metrics if self.nsims > 1: - self.output_coords_dict['glac_runoff_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_runoff_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_runoff_monthly_mad'] = { 'long_name': 'glacier-wide runoff median absolute deviation', @@ -460,10 +433,8 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'mass of ice based on area and ice thickness at start of the year', } - self.output_coords_dict['glac_mass_bsl_annual_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('year', self.year_values)] - ) + self.output_coords_dict['glac_mass_bsl_annual_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('year', self.year_values)] ) self.output_attrs_dict['glac_mass_bsl_annual_mad'] = { 'long_name': 'glacier mass below sea level median absolute deviation', @@ -480,10 +451,8 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'equilibrium line altitude is the elevation where the climatic mass balance is zero', } - self.output_coords_dict['offglac_runoff_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_runoff_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_runoff_monthly_mad'] = { 'long_name': 'off-glacier-wide runoff median absolute deviation', @@ -541,10 +510,8 @@ def _update_dicts(self): 'units': 'm3', 'temporal_resolution': 'monthly', } - self.output_coords_dict['glac_frontalablation_monthly'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_frontalablation_monthly'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_frontalablation_monthly'] = { 'long_name': 'glacier-wide frontal ablation, in water equivalent', @@ -555,10 +522,8 @@ def _update_dicts(self): 'waterline and subaqueous frontal melting below the waterline; positive values indicate mass lost like melt' ), } - self.output_coords_dict['glac_massbaltotal_monthly'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_massbaltotal_monthly'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_massbaltotal_monthly'] = { 'long_name': 'glacier-wide total mass balance, in water equivalent', @@ -575,10 +540,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'transient snowline is altitude separating snow from ice/firn', } - self.output_coords_dict['glac_mass_change_ignored_annual'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('year', self.year_values)] - ) + self.output_coords_dict['glac_mass_change_ignored_annual'] = collections.OrderedDict( + [('glac', self.glac_values), ('year', self.year_values)] ) self.output_attrs_dict['glac_mass_change_ignored_annual'] = { 'long_name': 'glacier mass change ignored', @@ -595,10 +558,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['offglac_refreeze_monthly'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_refreeze_monthly'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_refreeze_monthly'] = { 'long_name': 'off-glacier-wide refreeze, in water equivalent', @@ -614,10 +575,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only melt of snow and refreeze since off-glacier', } - self.output_coords_dict['offglac_snowpack_monthly'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_snowpack_monthly'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_snowpack_monthly'] = { 'long_name': 'off-glacier-wide snowpack, in water equivalent', @@ -628,10 +587,8 @@ def _update_dicts(self): # if nsims > 1, store median-absolute deviation metrics if self.nsims > 1: - self.output_coords_dict['glac_prec_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_prec_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_prec_monthly_mad'] = { 'long_name': 'glacier-wide precipitation (liquid) median absolute deviation', @@ -639,10 +596,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['glac_temp_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_temp_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_temp_monthly_mad'] = { 'standard_name': 'air_temperature', @@ -654,10 +609,8 @@ def _update_dicts(self): 'bins where the glacier no longer exists due to retreat have been removed' ), } - self.output_coords_dict['glac_acc_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_acc_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_acc_monthly_mad'] = { 'long_name': 'glacier-wide accumulation, in water equivalent, median absolute deviation', @@ -665,30 +618,24 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only the solid precipitation', } - self.output_coords_dict['glac_refreeze_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_refreeze_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_refreeze_monthly_mad'] = { 'long_name': 'glacier-wide refreeze, in water equivalent, median absolute deviation', 'units': 'm3', 'temporal_resolution': 'monthly', } - self.output_coords_dict['glac_melt_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_melt_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_melt_monthly_mad'] = { 'long_name': 'glacier-wide melt, in water equivalent, median absolute deviation', 'units': 'm3', 'temporal_resolution': 'monthly', } - self.output_coords_dict['glac_frontalablation_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_frontalablation_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_frontalablation_monthly_mad'] = { 'long_name': 'glacier-wide frontal ablation, in water equivalent, median absolute deviation', @@ -699,10 +646,8 @@ def _update_dicts(self): 'waterline and subaqueous frontal melting below the waterline' ), } - self.output_coords_dict['glac_massbaltotal_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_massbaltotal_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_massbaltotal_monthly_mad'] = { 'long_name': 'glacier-wide total mass balance, in water equivalent, median absolute deviation', @@ -710,10 +655,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'total mass balance is the sum of the climatic mass balance and frontal ablation', } - self.output_coords_dict['glac_snowline_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['glac_snowline_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['glac_snowline_monthly_mad'] = { 'long_name': 'transient snowline above mean sea level median absolute deviation', @@ -721,10 +664,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'transient snowline is altitude separating snow from ice/firn', } - self.output_coords_dict['glac_mass_change_ignored_annual_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('year', self.year_values)] - ) + self.output_coords_dict['glac_mass_change_ignored_annual_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('year', self.year_values)] ) self.output_attrs_dict['glac_mass_change_ignored_annual_mad'] = { 'long_name': 'glacier mass change ignored median absolute deviation', @@ -732,10 +673,8 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'glacier mass change ignored due to flux divergence', } - self.output_coords_dict['offglac_prec_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_prec_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_prec_monthly_mad'] = { 'long_name': 'off-glacier-wide precipitation (liquid) median absolute deviation', @@ -743,20 +682,16 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['offglac_refreeze_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_refreeze_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_refreeze_monthly_mad'] = { 'long_name': 'off-glacier-wide refreeze, in water equivalent, median absolute deviation', 'units': 'm3', 'temporal_resolution': 'monthly', } - self.output_coords_dict['offglac_melt_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_melt_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_melt_monthly_mad'] = { 'long_name': 'off-glacier-wide melt, in water equivalent, median absolute deviation', @@ -764,10 +699,8 @@ def _update_dicts(self): 'temporal_resolution': 'monthly', 'comment': 'only melt of snow and refreeze since off-glacier', } - self.output_coords_dict['offglac_snowpack_monthly_mad'] = ( - collections.OrderedDict( - [('glac', self.glac_values), ('time', self.time_values)] - ) + self.output_coords_dict['offglac_snowpack_monthly_mad'] = collections.OrderedDict( + [('glac', self.glac_values), ('time', self.time_values)] ) self.output_attrs_dict['offglac_snowpack_monthly_mad'] = { 'long_name': 'off-glacier-wide snowpack, in water equivalent, median absolute deviation', @@ -907,14 +840,12 @@ def _update_dicts(self): # optionally store binned mass balance components if self.binned_components: - self.output_coords_dict['bin_accumulation_monthly'] = ( - collections.OrderedDict( - [ - ('glac', self.glac_values), - ('bin', self.bin_values), - ('time', self.time_values), - ] - ) + self.output_coords_dict['bin_accumulation_monthly'] = collections.OrderedDict( + [ + ('glac', self.glac_values), + ('bin', self.bin_values), + ('time', self.time_values), + ] ) self.output_attrs_dict['bin_accumulation_monthly'] = { 'long_name': 'binned monthly accumulation, in water equivalent', @@ -977,14 +908,12 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'thickness of ice at start of the year', } - self.output_coords_dict['bin_massbalclim_annual_mad'] = ( - collections.OrderedDict( - [ - ('glac', self.glac_values), - ('bin', self.bin_values), - ('year', self.year_values), - ] - ) + self.output_coords_dict['bin_massbalclim_annual_mad'] = collections.OrderedDict( + [ + ('glac', self.glac_values), + ('bin', self.bin_values), + ('year', self.year_values), + ] ) self.output_attrs_dict['bin_massbalclim_annual_mad'] = { 'long_name': 'binned climatic mass balance, in water equivalent, median absolute deviation', @@ -1025,12 +954,8 @@ def calc_stats_array(data, stats_cns=pygem_prms['sim']['out']['sim_stats']): # calculate statustics for each stat in `stats_cns` with warnings.catch_warnings(): - warnings.simplefilter( - 'ignore', RuntimeWarning - ) # Suppress All-NaN Slice Warnings - stats_list = [ - stat_funcs[stat](data) for stat in stats_cns if stat in stat_funcs - ] + warnings.simplefilter('ignore', RuntimeWarning) # Suppress All-NaN Slice Warnings + stats_list = [stat_funcs[stat](data) for stat in stats_cns if stat in stat_funcs] # stack stats_list to numpy array return np.column_stack(stats_list) if stats_list else None diff --git a/pygem/plot/graphics.py b/pygem/plot/graphics.py new file mode 100644 index 00000000..74f0bbba --- /dev/null +++ b/pygem/plot/graphics.py @@ -0,0 +1,471 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2025 Brandon Tober , David Rounce + +Distributed under the MIT license + +Graphics module with various plotting tools +""" + +import warnings +from datetime import datetime + +import matplotlib.pyplot as plt +import numpy as np +from scipy.stats import binned_statistic + +from pygem.utils.stats import effective_n + + +def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): + """Plots the result of the model output along the flowline. + A paired down version of OGGMs graphics.plot_modeloutput_section() + + Parameters + ---------- + model: obj + either a FlowlineModel or a list of model flowlines. + fig + title + """ + + try: + fls = model.fls + except AttributeError: + fls = model + + if ax is None: + fig = plt.figure(figsize=(12, 6)) + ax = fig.add_axes([0.07, 0.08, 0.7, 0.84]) + else: + fig = plt.gcf() + + # Compute area histo + area = np.array([]) + height = np.array([]) + bed = np.array([]) + for cls in fls: + a = cls.widths_m * cls.dx_meter * 1e-6 + a = np.where(cls.thick > 0, a, 0) + area = np.concatenate((area, a)) + height = np.concatenate((height, cls.surface_h)) + bed = np.concatenate((bed, cls.bed_h)) + ylim = [bed.min(), height.max()] + + # plot Centerlines + cls = fls[-1] + x = np.arange(cls.nx) * cls.dx * cls.map_dx + + # Plot the bed + ax.plot(x, cls.bed_h, color='k', linewidth=2.5, label='Bed (Parab.)') + + # Plot glacier + t1 = cls.thick[:-2] + t2 = cls.thick[1:-1] + t3 = cls.thick[2:] + pnan = ((t1 == 0) & (t2 == 0)) & ((t2 == 0) & (t3 == 0)) + cls.surface_h[np.where(pnan)[0] + 1] = np.nan + + if 'srfcolor' in kwargs.keys(): + srfcolor = kwargs['srfcolor'] + else: + srfcolor = '#003399' + + if 'srfls' in kwargs.keys(): + srfls = kwargs['srfls'] + else: + srfls = '-' + + ax.plot(x, cls.surface_h, color=srfcolor, linewidth=2, ls=srfls, label='Glacier') + + # Plot tributaries + for i, inflow in zip(cls.inflow_indices, cls.inflows): + if inflow.thick[-1] > 0: + ax.plot( + x[i], + cls.surface_h[i], + 's', + markerfacecolor='#993399', + markeredgecolor='k', + label='Tributary (active)', + ) + else: + ax.plot( + x[i], + cls.surface_h[i], + 's', + markerfacecolor='w', + markeredgecolor='k', + label='Tributary (inactive)', + ) + if getattr(model, 'do_calving', False): + ax.hlines(model.water_level, x[0], x[-1], linestyles=':', color='C0') + + ax.set_ylim(ylim) + + ax.spines['top'].set_color('none') + ax.xaxis.set_ticks_position('bottom') + ax.set_xlabel('Distance along flowline (m)') + ax.set_ylabel('Altitude (m)') + + # Title + ax.set_title(title, loc='left') + + +def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show=False, fpath=None): + # Plot the trace of the parameters + n = m_primes.shape[1] + fig, axes = plt.subplots(n + 1, 1, figsize=(6, n * 1.5), sharex=True) + m_chain = m_chain.detach().numpy() + m_primes = m_primes.detach().numpy() + + # get n_eff + neff = [effective_n(arr) for arr in m_chain.T] + # instantiate list to hold legend objs + legs = [] + + axes[0].plot( + [], + [], + label=f'median={np.median(m_chain[:, 0]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, 0], [75, 25])):.3f}', + ) + l0 = axes[0].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(l0) + axes[0].plot(m_primes[:, 0], '.', ms=ms, label='proposed', c='tab:blue') + axes[0].plot(m_chain[:, 0], '.', ms=ms, label='accepted', c='tab:orange') + hands, ls = axes[0].get_legend_handles_labels() + + # axes[0].add_artist(leg) + axes[0].set_ylabel(r'$T_{bias}$', fontsize=fontsize) + + axes[1].plot(m_primes[:, 1], '.', ms=ms, c='tab:blue') + axes[1].plot(m_chain[:, 1], '.', ms=ms, c='tab:orange') + axes[1].plot( + [], + [], + label=f'median={np.median(m_chain[:, 1]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, 1], [75, 25])):.3f}', + ) + l1 = axes[1].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(l1) + axes[1].set_ylabel(r'$K_p$', fontsize=fontsize) + + axes[2].plot(m_primes[:, 2], '.', ms=ms, c='tab:blue') + axes[2].plot(m_chain[:, 2], '.', ms=ms, c='tab:orange') + axes[2].plot( + [], + [], + label=f'median={np.median(m_chain[:, 2]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, 2], [75, 25])):.3f}', + ) + l2 = axes[2].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(l2) + axes[2].set_ylabel(r'$fsnow$', fontsize=fontsize) + + if n > 4: + m_chain[:, 3] = m_chain[:, 3] + m_primes[:, 3] = m_primes[:, 3] + axes[3].plot(m_primes[:, 3], '.', ms=ms, c='tab:blue') + axes[3].plot(m_chain[:, 3], '.', ms=ms, c='tab:orange') + axes[3].plot( + [], + [], + label=f'median={np.median(m_chain[:, 3]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, 3], [75, 25])):.3f}', + ) + l3 = axes[3].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(l3) + axes[3].set_ylabel(r'$\rho_{abl}$', fontsize=fontsize) + + m_chain[:, 4] = m_chain[:, 4] + m_primes[:, 4] = m_primes[:, 4] + axes[4].plot(m_primes[:, 4], '.', ms=ms, c='tab:blue') + axes[4].plot(m_chain[:, 4], '.', ms=ms, c='tab:orange') + axes[4].plot( + [], + [], + label=f'median={np.median(m_chain[:, 4]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, 4], [75, 25])):.3f}', + ) + l4 = axes[4].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(l4) + axes[4].set_ylabel(r'$\rho_{acc}$', fontsize=fontsize) + + axes[-2].fill_between( + np.arange(len(ar)), + mb_obs[0] - (2 * mb_obs[1]), + mb_obs[0] + (2 * mb_obs[1]), + color='grey', + alpha=0.3, + ) + axes[-2].fill_between( + np.arange(len(ar)), + mb_obs[0] - mb_obs[1], + mb_obs[0] + mb_obs[1], + color='grey', + alpha=0.3, + ) + axes[-2].plot(m_primes[:, -1], '.', ms=ms, c='tab:blue') + axes[-2].plot(m_chain[:, -1], '.', ms=ms, c='tab:orange') + axes[-2].plot( + [], + [], + label=f'median={np.median(m_chain[:, -1]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, -1], [75, 25])):.3f}', + ) + ln2 = axes[-2].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(ln2) + axes[-2].set_ylabel(r'$\dot{{b}}$', fontsize=fontsize) + + axes[-1].plot(ar, 'tab:orange', lw=1) + axes[-1].plot( + np.convolve(ar, np.ones(100) / 100, mode='valid'), + 'k', + label='moving avg.', + lw=1, + ) + ln1 = axes[-1].legend(loc='upper left', handlelength=0.5, borderaxespad=0, fontsize=fontsize) + legs.append(ln1) + axes[-1].set_ylabel(r'$AR$', fontsize=fontsize) + + for i, ax in enumerate(axes): + ax.xaxis.set_ticks_position('both') + ax.yaxis.set_ticks_position('both') + ax.tick_params(axis='both', direction='inout') + if i == n: + continue + ax.plot([], [], label=f'n_eff={neff[i]}') + hands, ls = ax.get_legend_handles_labels() + if i == 0: + ax.legend( + handles=[hands[1], hands[2], hands[3]], + labels=[ls[1], ls[2], ls[3]], + loc='upper left', + borderaxespad=0, + handlelength=0, + fontsize=fontsize, + ) + else: + ax.legend( + handles=[hands[-1]], + labels=[ls[-1]], + loc='upper left', + borderaxespad=0, + handlelength=0, + fontsize=fontsize, + ) + + for i, ax in enumerate(axes): + ax.add_artist(legs[i]) + + axes[0].set_xlim([0, m_chain.shape[0]]) + axes[0].set_title(title, fontsize=fontsize) + plt.tight_layout() + plt.subplots_adjust(hspace=0.1, wspace=0) + if fpath: + fig.savefig(fpath, dpi=400) + if show: + plt.show(block=True) # wait until the figure is closed + plt.close(fig) + + +def plot_resid_histogram(obs, preds, title, fontsize=8, show=False, fpath=None): + # Plot the trace of the parameters + fig, axes = plt.subplots(1, 1, figsize=(3, 2)) + # subtract obs from preds to get residuals + diffs = np.concatenate([pred.flatten() - obs[0].flatten().numpy() for pred in preds]) + # mask nans to avoid error in np.histogram() + diffs = diffs[~np.isnan(diffs)] + # Calculate histogram counts and bin edges + counts, bin_edges = np.histogram(diffs, bins=20) + pct = counts / counts.sum() * 100 + bin_width = bin_edges[1] - bin_edges[0] + axes.bar( + bin_edges[:-1], + pct, + width=bin_width, + edgecolor='black', + color='gray', + align='edge', + ) + axes.set_xlabel('residuals (pred - obs)', fontsize=fontsize) + axes.set_ylabel('count (%)', fontsize=fontsize) + axes.set_title(title, fontsize=fontsize) + plt.tight_layout() + plt.subplots_adjust(hspace=0.1, wspace=0) + if fpath: + fig.savefig(fpath, dpi=400) + if show: + plt.show(block=True) # wait until the figure is closed + plt.close(fig) + + +def plot_mcmc_elev_change_1d( + preds, fls, obs, ela, title, fontsize=8, rate=True, uniform_area=True, show=False, fpath=None +): + bin_z = np.array(obs['bin_centers']) + bin_edges = np.array(obs['bin_edges']) + + # get initial thickness and surface area + initial_area = fls[0].widths_m * fls[0].dx_meter + initial_thickness = getattr(fls[0], 'thick', None) + initial_surface_h = getattr(fls[0], 'surface_h', None) + # sort initial surface height + sorting = np.argsort(initial_surface_h) + initial_surface_h = initial_surface_h[sorting] + initial_area = initial_area[sorting] + initial_thickness = initial_thickness[sorting] + # get first and last non-zero thickness indices + first, last = np.nonzero(initial_thickness)[0][[0, -1]] + # rebin surfce area + initial_area = binned_statistic(x=initial_surface_h, values=initial_area, statistic=np.nanmean, bins=bin_edges)[0] + # use reference dataset bin area if available + if 'bin_area' in obs: + initial_area = obs['bin_area'] + + if uniform_area: + xvals = np.nancumsum(initial_area) * 1e-6 + else: + xvals = bin_z + + # get date time spans + labels = [] + nyrs = [] + for start, end in obs['dates']: + labels.append(f'{start[:-2].replace("-", "")}:{end[:-3].replace("-", "")}') + start_dt = datetime.strptime(start, '%Y-%m-%d') + end_dt = datetime.strptime(end, '%Y-%m-%d') + nyrs.append((end_dt - start_dt).days / 365.25) + if not rate: + nyrs[:] = 1 + ylbl = 'Elevation change (m)' + else: + ylbl = r'Elevation change (m yr$^{-1}$)' + + # instantiate subplots + fig, ax = plt.subplots( + nrows=len(labels), + ncols=1, + figsize=(5, len(labels) * 2), + gridspec_kw={'hspace': 0.075}, + sharex=True, + sharey=rate, + ) + + # Transform functions + def cum_area_to_elev(x): + return np.interp(x, xvals, bin_z) + + def elev_to_cum_area(x): + return np.interp(x, bin_z, xvals) + + if not isinstance(ax, np.ndarray): + ax = [ax] + + # loop through date spans + for t in range(len(labels)): + ax[t].xaxis.set_label_position('top') + ax[t].xaxis.tick_top() # move ticks to top + ax[t].tick_params(axis='x', which='both', top=False) + ax[t].axhline(y=0, c='grey', lw=0.5) + preds = np.stack(preds) + + ax[t].fill_between( + xvals, + (obs['dh'][:, t] - obs['dh_sigma'][:, t]) / nyrs[t], + (obs['dh'][:, t] + obs['dh_sigma'][:, t]) / nyrs[t], + color='k', + alpha=0.125, + ) + ax[t].plot(xvals, obs['dh'][:, t] / nyrs[t], 'k-', marker='.', label='Obs.') + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + ax[t].fill_between( + xvals, + np.nanpercentile(preds[:, :, t], 5, axis=0) / nyrs[t], + np.nanpercentile(preds[:, :, t], 95, axis=0) / nyrs[t], + color='r', + alpha=0.25, + ) + ax[t].plot( + xvals, + np.nanmedian(preds[:, :, t], axis=0) / nyrs[t], + 'r-', + marker='.', + label='Pred.', + ) + + # for r in stack: + # axb.plot(bin_z, r, 'r', alpha=.0125) + + # dummy label for timespan + ax[t].text( + 0.99175, + 0.980, + labels[t], + transform=ax[t].transAxes, + fontsize=8, + verticalalignment='top', + horizontalalignment='right', + bbox=dict( + facecolor='white', + edgecolor='black', + alpha=1, + boxstyle='square,pad=0.25', + ), + zorder=10, + ) + + secaxx = ax[t].secondary_xaxis('bottom', functions=(cum_area_to_elev, elev_to_cum_area)) + + if t != len(labels) - 1: + secaxx.tick_params(axis='x', labelbottom=False) + else: + secaxx.set_xlabel('Elevation (m)') + + if t == 0: + leg = ax[t].legend( + handlelength=1, + borderaxespad=0, + fancybox=False, + loc='lower right', + edgecolor='k', + framealpha=1, + ) + for legobj in leg.legend_handles: + legobj.set_linewidth(2.0) + # Turn off cumulative area ticks and labels + ax[t].tick_params(axis='x', which='both', top=False, labeltop=False) + + ax[0].set_xlim(list(map(elev_to_cum_area, (initial_surface_h[first], initial_surface_h[last])))) + + for a in ax: + # plot ela + a.axvline(x=elev_to_cum_area(ela), c='k', ls=':', lw=1) + + ax[-1].text( + 0.0125, + 0.5, + ylbl, + horizontalalignment='left', + rotation=90, + verticalalignment='center', + transform=fig.transFigure, + ) + + ax[0].set_title(title, fontsize=fontsize) + # Remove overlapping tick labels from secaxx + fig.canvas.draw() # Force rendering to get accurate bounding boxes + labels = secaxx.get_xticklabels() + renderer = fig.canvas.get_renderer() + bboxes = [label.get_window_extent(renderer) for label in labels] + # Only show labels spaced apart by at least `min_spacing` pixels + min_spacing = 15 # adjust as needed + last_right = -float('inf') + for label, bbox in zip(labels, bboxes): + if bbox.x0 > last_right + min_spacing: + last_right = bbox.x1 + else: + label.set_visible(False) + # save + if fpath: + fig.savefig(fpath, dpi=400) + if show: + plt.show(block=True) # wait until the figure is closed + plt.close(fig) diff --git a/pygem/pygem_modelsetup.py b/pygem/pygem_modelsetup.py index d47894ec..9524b878 100755 --- a/pygem/pygem_modelsetup.py +++ b/pygem/pygem_modelsetup.py @@ -76,9 +76,7 @@ def datesmodelrun( if pygem_prms['time']['timestep'] == 'monthly': # Automatically generate dates from start date to end data using a monthly frequency (MS), which generates # monthly data using the 1st of each month' - dates_table = pd.DataFrame( - {'date': pd.date_range(startdate, enddate, freq='MS', unit='s')} - ) + dates_table = pd.DataFrame({'date': pd.date_range(startdate, enddate, freq='MS', unit='s')}) # Select attributes of DateTimeIndex (dt.year, dt.month, and dt.daysinmonth) dates_table['year'] = dates_table['date'].dt.year dates_table['month'] = dates_table['date'].dt.month @@ -92,9 +90,7 @@ def datesmodelrun( dates_table.loc[mask1, 'daysinmonth'] = 28 elif pygem_prms['time']['timestep'] == 'daily': # Automatically generate daily (freq = 'D') dates - dates_table = pd.DataFrame( - {'date': pd.date_range(startdate, enddate, freq='D')} - ) + dates_table = pd.DataFrame({'date': pd.date_range(startdate, enddate, freq='D')}) # Extract attributes for dates_table dates_table['year'] = dates_table['date'].dt.year dates_table['month'] = dates_table['date'].dt.month @@ -112,9 +108,7 @@ def datesmodelrun( mask2 = (dates_table['month'] == 2) & (dates_table['day'] == 29) dates_table.drop(dates_table[mask2].index, inplace=True) else: - print( - "\n\nError: Please select 'daily' or 'monthly' for gcm_timestep. Exiting model run now.\n" - ) + print("\n\nError: Please select 'daily' or 'monthly' for gcm_timestep. Exiting model run now.\n") exit() # Add column for water year # Water year for northern hemisphere using USGS definition (October 1 - September 30th), @@ -197,8 +191,7 @@ def hypsometrystats(hyps_table, thickness_table): # Mean glacier elevation glac_hyps_mean = np.zeros(glac_volume.shape) glac_hyps_mean[glac_volume > 0] = ( - hyps_table[glac_volume > 0].values - * hyps_table[glac_volume > 0].columns.values.astype(int) + hyps_table[glac_volume > 0].values * hyps_table[glac_volume > 0].columns.values.astype(int) ).sum(axis=1) / hyps_table[glac_volume > 0].values.sum(axis=1) # Median computations # main_glac_hyps_cumsum = np.cumsum(hyps_table, axis=1) @@ -242,9 +235,7 @@ def import_Husstable( for count, region in enumerate(rgi_regionsO1): # Select regional data for indexing glac_no = sorted(glac_no_byregion[region]) - rgi_table_region = rgi_table.iloc[ - np.where(rgi_table.O1Region.values == region)[0] - ] + rgi_table_region = rgi_table.iloc[np.where(rgi_table.O1Region.values == region)[0]] # Load table ds = pd.read_csv(filepath + filedict[region]) @@ -353,9 +344,7 @@ def selectglaciersrgitable( (rows = GlacNo, columns = glacier statistics) """ doc_url = 'https://pygem.readthedocs.io/en/latest/model_inputs.html#model-inputs' - doc_meessage = ( - 'Has the full dataset been downloaded? See documentation for more information:' - ) + doc_meessage = 'Has the full dataset been downloaded? See documentation for more information:' if glac_no is not None: glac_no_byregion = {} rgi_regionsO1 = [int(i.split('.')[0]) for i in glac_no] @@ -397,10 +386,7 @@ def selectglaciersrgitable( # Populate glacer_table with the glaciers of interest if rgi_regionsO2 == 'all' and rgi_glac_number == 'all': if debug: - print( - 'All glaciers within region(s) %s are included in this model run.' - % (region) - ) + print('All glaciers within region(s) %s are included in this model run.' % (region)) if glacier_table.empty: glacier_table = csv_regionO1 else: @@ -413,9 +399,7 @@ def selectglaciersrgitable( ) for regionO2 in rgi_regionsO2: if glacier_table.empty: - glacier_table = csv_regionO1.loc[ - csv_regionO1['O2Region'] == regionO2 - ] + glacier_table = csv_regionO1.loc[csv_regionO1['O2Region'] == regionO2] else: glacier_table = pd.concat( [ @@ -436,17 +420,13 @@ def selectglaciersrgitable( % (len(rgi_glac_number), region, rgi_glac_number[0:50]) ) - rgiid_subset = [ - 'RGI60-' + str(region).zfill(2) + '.' + x for x in rgi_glac_number - ] + rgiid_subset = ['RGI60-' + str(region).zfill(2) + '.' + x for x in rgi_glac_number] rgiid_all = list(csv_regionO1.RGIId.values) rgi_idx = [rgiid_all.index(x) for x in rgiid_subset if x in rgiid_all] if glacier_table.empty: glacier_table = csv_regionO1.loc[rgi_idx] else: - glacier_table = pd.concat( - [glacier_table, csv_regionO1.loc[rgi_idx]], axis=0 - ) + glacier_table = pd.concat([glacier_table, csv_regionO1.loc[rgi_idx]], axis=0) glacier_table = glacier_table.copy() # reset the index so that it is in sequential order (0, 1, 2, etc.) @@ -459,14 +439,10 @@ def selectglaciersrgitable( # Record the reference date glacier_table['RefDate'] = glacier_table['BgnDate'] # if there is an end date, then roughly average the year - enddate_idx = glacier_table.loc[ - (glacier_table['EndDate'] > 0), 'EndDate' - ].index.values + enddate_idx = glacier_table.loc[(glacier_table['EndDate'] > 0), 'EndDate'].index.values glacier_table.loc[enddate_idx, 'RefDate'] = ( np.mean( - ( - glacier_table.loc[enddate_idx, ['BgnDate', 'EndDate']].values / 10**4 - ).astype(int), + (glacier_table.loc[enddate_idx, ['BgnDate', 'EndDate']].values / 10**4).astype(int), axis=1, ).astype(int) * 10**4 @@ -475,9 +451,7 @@ def selectglaciersrgitable( # drop columns of data that is not being used glacier_table.drop(rgi_cols_drop, axis=1, inplace=True) # add column with the O1 glacier numbers - glacier_table[rgi_O1Id_colname] = ( - glacier_table['RGIId'].str.split('.').apply(pd.Series).loc[:, 1].astype(int) - ) + glacier_table[rgi_O1Id_colname] = glacier_table['RGIId'].str.split('.').apply(pd.Series).loc[:, 1].astype(int) glacier_table['rgino_str'] = [x.split('-')[1] for x in glacier_table.RGIId.values] # glacier_table[rgi_glacno_float_colname] = (np.array([np.str.split(glacier_table['RGIId'][x],'-')[1] # for x in range(glacier_table.shape[0])]).astype(float)) @@ -511,8 +485,7 @@ def selectglaciersrgitable( glacier_table.reset_index(inplace=True, drop=True) # Glacier number with no trailing zeros glacier_table['glacno'] = [ - str(int(x.split('-')[1].split('.')[0])) + '.' + x.split('-')[1].split('.')[1] - for x in glacier_table.RGIId + str(int(x.split('-')[1].split('.')[0])) + '.' + x.split('-')[1].split('.')[1] for x in glacier_table.RGIId ] # Remove glaciers below threshold @@ -527,10 +500,7 @@ def selectglaciersrgitable( glacier_table = glacier_table.loc[unique_idx, :] glacier_table.reset_index(inplace=True, drop=True) - print( - 'This study is focusing on %s glaciers in region %s' - % (glacier_table.shape[0], rgi_regionsO1) - ) + print('This study is focusing on %s glaciers in region %s' % (glacier_table.shape[0], rgi_regionsO1)) return glacier_table @@ -610,9 +580,7 @@ def split_list(lst, n=1, option_ordered=1, group_thousands=False): lst_batches_th = [] # keep the number of batches, but move items around to not have sets of RGIXX.YY ids in more than one batch for s in sets: - merged = [ - item for sublist in lst_batches for item in sublist if item[:5] == s - ] + merged = [item for sublist in lst_batches for item in sublist if item[:5] == s] lst_batches_th.append(merged) # ensure that number of batches doesn't exceed original number while len(lst_batches_th) > n: diff --git a/pygem/setup/config.py b/pygem/setup/config.py index 5ab8e51e..c60f520c 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -29,9 +29,7 @@ def __init__(self, config_filename='config.yaml', base_dir=None, overwrite=False self.config_filename = config_filename self.base_dir = base_dir or os.path.join(os.path.expanduser('~'), 'PyGEM') self.config_path = os.path.join(self.base_dir, self.config_filename) - self.source_config_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), 'config.yaml' - ) + self.source_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yaml') self.overwrite = overwrite self._ensure_config() @@ -109,9 +107,7 @@ def _validate_config(self, config): raise KeyError(f'Missing required key in configuration: {key}') if not isinstance(sub_data, expected_type): - raise TypeError( - f"Invalid type for '{key}': expected {expected_type}, not {type(sub_data)}" - ) + raise TypeError(f"Invalid type for '{key}': expected {expected_type}, not {type(sub_data)}") # Check elements inside lists (if defined) if key in self.LIST_ELEMENT_TYPES and isinstance(sub_data, list): @@ -121,6 +117,8 @@ def _validate_config(self, config): f"Invalid type for elements in '{key}': expected all elements to be {elem_type}, but got {sub_data}" ) + # check that all defined paths exist, raise error for any critical ones + # expected config types EXPECTED_TYPES = { 'root': str, @@ -222,8 +220,6 @@ def _validate_config(self, config): 'calib.emulator_params.ftol_opt': float, 'calib.emulator_params.eps_opt': float, 'calib.MCMC_params': dict, - 'calib.MCMC_params.option_calib_meltextent_1d': bool, - 'calib.MCMC_params.option_calib_snowline_1d': bool, 'calib.MCMC_params.option_use_emulator': bool, 'calib.MCMC_params.emulator_sims': int, 'calib.MCMC_params.tbias_step': float, @@ -253,6 +249,15 @@ def _validate_config(self, config): 'calib.MCMC_params.kp_sigma': float, 'calib.MCMC_params.kp_bndlow': float, 'calib.MCMC_params.kp_bndhigh': float, + 'calib.MCMC_params.option_calib_elev_change_1d': bool, + 'calib.MCMC_params.rhoabl_disttype': str, + 'calib.MCMC_params.rhoabl_mu': (int, float), + 'calib.MCMC_params.rhoabl_sigma': (int, float), + 'calib.MCMC_params.rhoaccum_disttype': str, + 'calib.MCMC_params.rhoaccum_mu': (int, float), + 'calib.MCMC_params.rhoaccum_sigma': (int, float), + 'calib.MCMC_params.option_calib_meltextent_1d': bool, + 'calib.MCMC_params.option_calib_snowline_1d': bool, 'calib.data': dict, 'calib.data.massbalance': dict, 'calib.data.massbalance.hugonnet2021_relpath': str, @@ -263,9 +268,13 @@ def _validate_config(self, config): 'calib.data.frontalablation.frontalablation_cal_fn': str, 'calib.data.icethickness': dict, 'calib.data.icethickness.h_ref_relpath': str, - 'calib.icethickness_cal_frac_byarea': float, + 'calib.data.elev_change_1d': dict, + 'calib.data.elev_change_1d.elev_change_1d_relpath': (str, type(None)), + 'calib.data.meltextent_1d': dict, 'calib.data.meltextent_1d.meltextent_1d_relpath': (str, type(None)), + 'calib.data.snowline_1d': dict, 'calib.data.snowline_1d.snowline_1d_relpath': (str, type(None)), + 'calib.icethickness_cal_frac_byarea': float, 'sim': dict, 'sim.option_dynamics': (str, type(None)), 'sim.option_bias_adjustment': int, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 5fce2d1c..83bf12b8 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -150,8 +150,6 @@ calib: # MCMC params MCMC_params: - option_calib_meltextent_1d: false # option to calibrate against 1d melt extent data (true or false) - option_calib_snowline_1d: false # option to calibrate against 1d snowline data (true or false) option_use_emulator: true # use emulator or full model (if true, calibration must have first been run with option_calibretion=='emulator') emulator_sims: 100 tbias_step: 0.1 @@ -185,7 +183,19 @@ calib: kp_sigma: 1.5 # precipitation factor standard deviation of normal distribution kp_bndlow: 0.5 # precipitation factor lower bound kp_bndhigh: 1.5 # precipitation factor upper bound - + # options for calibrating against 1d elevation change + option_calib_elev_change_1d: false # option to calibrate against 1d elevation change data (true or false) + rhoabl_disttype: normal # ablation area density prior distribution type (currently only 'normal' option) + rhoabl_mu: 900 # ablation area density prior mean (kg m^-3) + rhoabl_sigma: 17 # ablation area density prior standard deviation (kg m^-3) + rhoaccum_disttype: normal # ablation area density prior distribution type (currently only 'normal' option) + rhoaccum_mu: 600 # accumulation area density prior mean (kg m^-3). default value from Huss, 2013 Table 1 + rhoaccum_sigma: 60 # accumulation area density prior standard deviation (kg m^-3). default value from Huss, 2013 Table 1 + # options for calibrating against 1d melt extent data + option_calib_meltextent_1d: false # option to calibrate against 1d melt extent data (true or false) + # options for calibrating against 1d snowline data + option_calib_snowline_1d: false # option to calibrate against 1d snowline data (true or false) + # calibration datasets data: # mass balance data @@ -199,14 +209,16 @@ calib: frontalablation_cal_fn: all-frontalablation_cal_ind.csv # merged frontalablation calibration data (produced by run_calibration_frontalablation.py) # ice thickness icethickness: - h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ + h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ + # 1d elevation change + elev_change_1d: + elev_change_1d_relpath: /elev_change_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 1.00570_elev_change_1d_.json) # 1d melt extents meltextent_1d: - meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) + meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) # 1d snowlines snowline_1d: - snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) - + snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) icethickness_cal_frac_byarea: 0.9 # Regional glacier area fraction that is used to calibrate the ice thickness # e.g., 0.9 means only the largest 90% of glaciers by area will be used to calibrate diff --git a/pygem/shop/debris.py b/pygem/shop/debris.py index 66c548f4..247017a8 100755 --- a/pygem/shop/debris.py +++ b/pygem/shop/debris.py @@ -71,9 +71,7 @@ def debris_to_gdir( hd_dir = debris_dir + 'hd_tifs/' + gdir.rgi_region + '/' ed_dir = debris_dir + 'ed_tifs/' + gdir.rgi_region + '/' - glac_str_nolead = ( - str(int(gdir.rgi_region)) + '.' + gdir.rgi_id.split('-')[1].split('.')[1] - ) + glac_str_nolead = str(int(gdir.rgi_region)) + '.' + gdir.rgi_id.split('-')[1].split('.')[1] # If debris thickness data exists, then write to glacier directory if os.path.exists(hd_dir + glac_str_nolead + '_hdts_m.tif'): @@ -157,9 +155,7 @@ def debris_to_gdir( @entity_task(log, writes=['inversion_flowlines']) -def debris_binned( - gdir, ignore_debris=False, fl_str='inversion_flowlines', filesuffix='' -): +def debris_binned(gdir, ignore_debris=False, fl_str='inversion_flowlines', filesuffix=''): """Bin debris thickness and melt enhancement factors. Parameters @@ -178,9 +174,7 @@ def debris_binned( flowlines = gdir.read_pickle(fl_str, filesuffix=filesuffix) fl = flowlines[0] - assert len(flowlines) == 1, ( - 'Error: binning debris only works for single flowlines at present' - ) + assert len(flowlines) == 1, 'Error: binning debris only works for single flowlines at present' except: flowlines = None @@ -216,9 +210,7 @@ def debris_binned( for nbin in np.arange(0, len(z_bin_edges) - 1): bin_max = z_bin_edges[nbin] bin_min = z_bin_edges[nbin + 1] - bin_idx = np.where((topo_onglac < bin_max) & (topo_onglac >= bin_min))[ - 0 - ] + bin_idx = np.where((topo_onglac < bin_max) & (topo_onglac >= bin_min))[0] # Debris thickness and enhancement factors for on-glacier bins if len(bin_idx) > 0: with warnings.catch_warnings(): diff --git a/pygem/shop/elevchange1d.py b/pygem/shop/elevchange1d.py new file mode 100644 index 00000000..4bafd7a7 --- /dev/null +++ b/pygem/shop/elevchange1d.py @@ -0,0 +1,307 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2025 Brandon Tober , David Rounce + +Distributed under the MIT license +""" + +# Built-in libaries +import datetime +import json +import logging +import os + +import numpy as np +import pandas as pd + +# External libraries +# Local libraries +from oggm import cfg +from oggm.utils import entity_task + +# pygem imports +from pygem.setup.config import ConfigManager + +# instantiate ConfigManager +config_manager = ConfigManager() +# read the config +pygem_prms = config_manager.read_config() + + +# Module logger +log = logging.getLogger(__name__) + +# Add the new name "elev_change_1d" to the list of things that the GlacierDirectory understands +if 'elev_change_1d' not in cfg.BASENAMES: + cfg.BASENAMES['elev_change_1d'] = ( + 'elev_change_1d.json', + '1D elevation change data', + ) + + +@entity_task(log, writes=['elev_change_1d']) +def elev_change_1d_to_gdir( + gdir, +): + """ + Add 1D elevation change observations to the given glacier directory. + + Binned 1D elevation change data should be stored as a JSON or CSV file with the following equivalent formats. + + JSON file structure: + { + 'ref_dem': str, + 'ref_dem_year': int, + 'dates': [ + (date_start_1, date_end_1), + (date_start_2, date_end_2), + ... + (date_start_M, date_end_M) + ], + 'bin_edges': [edge0, edge1, ..., edgeN], + 'bin_area': [area0, area1, ..., areaN-1], + 'dh': [ + [dh_bin1_period1, dh_bin2_period1, ..., dh_binN-1_period1], + [dh_bin1_period2, dh_bin2_period2, ..., dh_binN-1_period2], + ... + [dh_bin1_periodM, dh_bin2_periodM, ..., dh_binN-1_periodM] + ], + 'dh_sigma': [ + [dh_sigma_bin1_period1, dh_sigma_bin2_period1, ..., dh_sigma_binN-1_period1], + [dh_sigma_bin1_period2, dh_sigma_bin2_period2, ..., dh_sigma_binN-1_period2], + ... + [dh_sigma_bin1_periodM, dh_sigma_bin2_periodM, ..., dh_sigma_binN-1_periodM] + ], + } + + Notes: + - 'ref_dem' is the reference DEM used for elevation-binning. + - 'ref_dem_year' is the acquisition year of the reference DEM. + - Each element in 'dates' defines one elevation change period with a start and end date, + stored as strings in 'YYYY-MM-DD' format. + - 'bin_edges' should be a list length N containing the elevation values of each bin edge. + - 'bin_area' should be a list of length N-1 containing the bin areas given by the 'ref_dem' (optional). + - 'dh' should contain M lists of length N-1, where M is the number of periods and N is the number of bin edges. Units are in meters. + - 'dh_sigma' should either be M lists of length N-1 (matching 'dh') or a single scalar value. Units are in meters. + - Each list in 'dh' (and optionally 'dh_sigma') corresponds exactly to one period in 'dates'. + + CSV file structure: + bin_start, bin_stop, bin_area, date_start, date_end, dh, dh_sigma, ref_dem, ref_dem_year + edge0, edge1, area0, date_start_1, date_end_1, dh_bin1_period1, dh_sigma_bin1_period1, ref_dem, ref_dem_year + edge1, edge2, area1, date_start_1, date_end_1, dh_bin2_period1, dh_sigma_bin2_period1, ref_dem, ref_dem_year + ... + edgeN-1, edgeN, areaN-1, date_start_1, date_end_1, dh_binN-1_period1, dh_sigma_binN-1_period1, ref_dem, ref_dem_year + edge0, edge1, area0, date_start_2, date_end_2, dh_bin1_period2, dh_sigma_bin1_period2, ref_dem, ref_dem_year + ... + edgeN-1, edgeN, areaN-1, date_start_M, date_end_M, dh_binN-1_periodM, dh_sigma_binN-1_periodM, ref_dem, ref_dem_year + + Notes: + - Each set of 'date_start' and 'date_end' defines one elevation change period. + - Dates must be stored as strings in 'YYYY-MM-DD' format. + - Rows with the same ('date_start', 'date_end') values correspond to a single period, + with one row per elevation bin. + - 'bin_area' should contain M × (N-1) entries, where M is the number of periods and N is the number of bin edges. Units are in square meters. (optional). + - 'dh' should contain M × (N-1) entries, where M is the number of periods and N is the number of bin edges. Units are in meters. + - 'dh_sigma' should contain M × (N-1) entries, where M is the number of periods and N is the number of bin edges. Units are in meters. + - 'ref_dem' is constant for all rows and indicates the acquisition year of the reference DEM used for elevation-binning. + - 'ref_dem_year' is constant for all rows and indicates the year of the reference DEM. + + Parameters + ---------- + gdir : :py:class:`oggm.GlacierDirectory` + where to write the data + + """ + # get dataset file path + elev_change_1d_fp = ( + f'{pygem_prms["root"]}/' + f'{pygem_prms["calib"]["data"]["elev_change_1d"]["elev_change_1d_relpath"]}/' + f'{gdir.rgi_id.split("-")[1]}_elev_change_1d' + ) + + # Check for both .json and .csv extensions + if os.path.exists(elev_change_1d_fp + '.json'): + elev_change_1d_fp += '.json' + with open(elev_change_1d_fp, 'r') as f: + data = json.load(f) + + elif os.path.exists(elev_change_1d_fp + '.csv'): + elev_change_1d_fp += '.csv' + data = csv_to_elev_change_1d_dict(elev_change_1d_fp) + + else: + log.debug('No binned elevation change data to load, skipping task.') + raise Warning('No binned elevation data to load') # file not found, skip + + validate_elev_change_1d_structure(data) + + gdir.write_json(data, 'elev_change_1d') + + +def validate_elev_change_1d_structure(data): + """Validate that elev_change_1d JSON structure matches expected format.""" + + required_keys = ['ref_dem', 'ref_dem_year', 'bin_edges', 'dates', 'dh', 'dh_sigma'] + for key in required_keys: + if key not in data: + raise ValueError(f"Missing required key '{key}' in elevation change JSON.") + + # Validate bin_edges + bin_edges = data['bin_edges'] + if not isinstance(bin_edges, list) or len(bin_edges) < 2: + raise ValueError("'bin_edges' must be a list of at least two numeric values.") + if not all(isinstance(x, (int, float)) for x in bin_edges): + raise ValueError("All 'bin_edges' values must be numeric.") + + # Calculate bin_centers if missing + if 'bin_centers' not in data: + data['bin_centers'] = [0.5 * (bin_edges[i] + bin_edges[i + 1]) for i in range(len(bin_edges) - 1)] + + # Validate bin_centers + bin_centers = data['bin_centers'] + if not isinstance(bin_centers, list) or len(bin_centers) != len(bin_edges) - 1: + raise ValueError("'bin_centers' must be a list of length len(bin_edges)-1.") + if not all(isinstance(x, (int, float)) for x in bin_centers): + raise ValueError("All 'bin_centers' values must be numeric.") + + # Validate dates + dates = data['dates'] + if not isinstance(dates, list) or len(dates) == 0: + raise ValueError("'dates' must be a non-empty list of (start, end) tuples.") + for i, d in enumerate(dates): + if not (isinstance(d, (list, tuple)) and len(d) == 2): + raise ValueError(f"'dates[{i}]' must be a 2-element list or tuple.") + for j, date_str in enumerate(d): + try: + datetime.datetime.strptime(date_str, '%Y-%m-%d') + except ValueError: + raise ValueError(f"Invalid date format in 'dates[{i}][{j}]': {date_str}") from None + + # Validate dh + dh = data['dh'] + M = len(dates) + N = len(bin_edges) - 1 + if not (isinstance(dh, list) and len(dh) == M): + raise ValueError(f"'dh' must have {M} entries, one per period in 'dates'.") + for i, arr in enumerate(dh): + if not (isinstance(arr, list) and len(arr) == N): + raise ValueError(f"'dh[{i}]' must be a list of length {N}.") + if not all(isinstance(x, (int, float)) for x in arr): + raise ValueError(f"All 'dh[{i}]' values must be numeric.") + + # Validate sigma + sigma = data['dh_sigma'] + if isinstance(sigma, (int, float)): + # scalar is OK + pass + elif isinstance(sigma, list): + if len(sigma) != M: + raise ValueError(f"'sigma' must have {M} entries to match 'dates'.") + for i, arr in enumerate(sigma): + if isinstance(arr, list): + if len(arr) != N: + raise ValueError(f"'sigma[{i}]' must be length {N}.") + if not all(isinstance(x, (int, float)) for x in arr): + raise ValueError(f"All 'sigma[{i}]' values must be numeric.") + elif not isinstance(arr, (int, float)): + raise ValueError(f"'sigma[{i}]' must be numeric or a list of numeric values.") + else: + raise ValueError("'sigma' must be a list or scalar numeric value.") + + # Validate ref_dem + ref_dem = data['ref_dem'] + if not isinstance(ref_dem, str): + raise ValueError("'ref_dem' must be a string.") + + # Validate ref_dem_year + ref_dem_year = data['ref_dem_year'] + if not isinstance(ref_dem_year, int): + raise ValueError("'ref_dem_year' must be an integer year.") + + # Validate bin_area if present + if 'bin_area' in data: + bin_area = data['bin_area'] + if len(bin_area) != N: + raise ValueError(f"'bin_area' must be a list of length {N}.") + if not all(isinstance(x, (int, float)) for x in bin_area): + raise ValueError("All 'bin_area' values must be numeric.") + + return True + + +def csv_to_elev_change_1d_dict(csv_path): + """ + Convert a CSV with columns: + bin_start, bin_stop, date_start, date_end, dh, dh_sigma, ref_dem, ref_dem_year + into a dictionary structure matching elev_change_data format. + """ + df = pd.read_csv(csv_path) + + required_cols = { + 'bin_start', + 'bin_stop', + 'date_start', + 'date_end', + 'dh', + 'dh_sigma', + 'ref_dem', + 'ref_dem_year', + } + if not required_cols.issubset(df.columns): + raise ValueError(f'CSV must contain columns: {required_cols}') + + # Ensure sorted bins + df = df.sort_values(['bin_start', 'date_start', 'date_end']).reset_index(drop=True) + + # Get all unique bin edges + bin_edges = sorted(set(df['bin_start']).union(df['bin_stop'])) + + if 'bin_area' in df.keys(): + bin_area = df['bin_area'].tolist() + else: + bin_area = False + + # Validate reference DEM - should only be one unique string + dems = df['ref_dem'].dropna().unique() + if len(dems) != 1: + raise ValueError(f"'ref_dem' must have exactly one unique value, but found {len(dems)}: {dems.tolist()}") + dem = dems[0] + if not isinstance(dem, str): + raise TypeError(f"'ref_dem' must be a string, but got {type(dem).__name__}: {dem}") + + # Validate reference DEM year - should only be one constant integer value + years = df['ref_dem_year'].dropna().unique() + if len(years) != 1: + raise ValueError(f"'ref_dem_year' must have exactly one unique value, but found {len(years)}: {years.tolist()}") + dem_year = years[0] + if not isinstance(dem_year, (int, np.integer)): + raise TypeError(f"'ref_dem_year' must be an integer, but got {type(dem_year).__name__}: {dem_year}") + # convert to plain int + dem_year = int(dem_year) + + # Get all unique date pairs (preserving order) + date_pairs = df[['date_start', 'date_end']].drop_duplicates().apply(tuple, axis=1).tolist() + + # Group by date pairs and collect dh, sigma + dh_all, sigma_all = [], [] + for ds, de in date_pairs: + subset = df[(df['date_start'] == ds) & (df['date_end'] == de)] + subset = subset.sort_values('bin_start') + dh_all.append(subset['dh'].tolist()) + sigma_all.append(subset['dh_sigma'].tolist()) + + data = { + 'ref_dem': dem, + 'ref_dem_year': dem_year, + 'dates': [(str(ds), str(de)) for ds, de in date_pairs], + 'bin_edges': bin_edges, + 'bin_area': bin_area, + 'dh': dh_all, + 'dh_sigma': sigma_all, + } + + if not bin_area: # remove bin_area if flag is False + del data['bin_area'] + + return data diff --git a/pygem/shop/icethickness.py b/pygem/shop/icethickness.py index a17cfbb4..9d5dd314 100755 --- a/pygem/shop/icethickness.py +++ b/pygem/shop/icethickness.py @@ -57,26 +57,15 @@ def consensus_gridded( where to write the data """ # If binned mb data exists, then write to glacier directory - h_fn = ( - h_consensus_fp - + 'RGI60-' - + gdir.rgi_region - + '/' - + gdir.rgi_id - + '_thickness.tif' - ) - assert os.path.exists(h_fn), ( - 'Error: h_consensus_fullfn for ' + gdir.rgi_id + ' does not exist.' - ) + h_fn = h_consensus_fp + 'RGI60-' + gdir.rgi_region + '/' + gdir.rgi_id + '_thickness.tif' + assert os.path.exists(h_fn), 'Error: h_consensus_fullfn for ' + gdir.rgi_id + ' does not exist.' # open consensus ice thickness estimate h_dr = rasterio.open(h_fn, 'r', driver='GTiff') h = h_dr.read(1).astype(rasterio.float32) # Glacier mass [kg] - glacier_mass_raw = (h * h_dr.res[0] * h_dr.res[1]).sum() * pygem_prms['constants'][ - 'density_ice' - ] + glacier_mass_raw = (h * h_dr.res[0] * h_dr.res[1]).sum() * pygem_prms['constants']['density_ice'] # print(glacier_mass_raw) if add_mass: @@ -98,9 +87,7 @@ def consensus_gridded( # Pixel area pixel_m2 = abs(gdir.grid.dx * gdir.grid.dy) # Glacier mass [kg] reprojoected (may lose or gain mass depending on resampling algorithm) - glacier_mass_reprojected = (data * pixel_m2).sum() * pygem_prms[ - 'constants' - ]['density_ice'] + glacier_mass_reprojected = (data * pixel_m2).sum() * pygem_prms['constants']['density_ice'] # Scale data to ensure conservation of mass during reprojection data_scaled = data * glacier_mass_raw / glacier_mass_reprojected # glacier_mass = (data_scaled * pixel_m2).sum() * pygem_prms['constants']['density_ice'] @@ -139,9 +126,7 @@ def consensus_binned(gdir): flowlines = gdir.read_pickle('inversion_flowlines') fl = flowlines[0] - assert len(flowlines) == 1, ( - 'Error: binning debris data set up only for single flowlines at present' - ) + assert len(flowlines) == 1, 'Error: binning debris data set up only for single flowlines at present' # Add binned debris thickness and enhancement factors to flowlines ds = xr.open_dataset(gdir.get_filepath('gridded_data')) diff --git a/pygem/shop/loso25icebridge.py b/pygem/shop/loso25icebridge.py new file mode 100644 index 00000000..0ff1ec8d --- /dev/null +++ b/pygem/shop/loso25icebridge.py @@ -0,0 +1,622 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2024 Brandon Tober , David Rounce + +Distributed under the MIT license + +NASA Operation IceBridge data and processing class +""" + +import datetime +import glob +import json +import os +import re +import warnings +from datetime import timedelta + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from scipy import stats + +# pygem imports +from pygem.setup.config import ConfigManager + +# instantiate ConfigManager +config_manager = ConfigManager() +# read the config +pygem_prms = config_manager.read_config() + +__all__ = ['oib'] + + +class oib: + def __init__( + self, + oib_datpath, + rgi6id='', + rgi7id='', + rgi7_rgi6_linksfp='RGI2000-v7.0-G-01_alaska-rgi6_links.csv', + ): + self.oib_datpath = oib_datpath + if os.path.isfile(rgi7_rgi6_linksfp): + self.rgi7_6_df = pd.read_csv(rgi7_rgi6_linksfp) + self.rgi7_6_df['rgi7_id'] = self.rgi7_6_df['rgi7_id'].str.split('RGI2000-v7.0-G-').str[1] + self.rgi7_6_df['rgi6_id'] = self.rgi7_6_df['rgi6_id'].str.split('RGI60-').str[1] + self.rgi6id = rgi6id + self.rgi7id = rgi7id + self.name = None + # instatntiate dictionary to hold all data - store the data by survey date, with each key containing a list with the binned differences and uncertainties (diffs, sigma) + self.oib_diffs = {} + self.dbl_diffs = {} + self.bin_edges = None + self.bin_centers = None + self.bin_area = None + # automatically map rgi6 and rgi7 ids if one is provided + if self.rgi6id and not self.rgi7id: + self._rgi6torgi7id() + elif self.rgi7id and not self.rgi6id: + self._rgi7torgi6id() + + def get_diffs(self): + return self.oib_diffs + + def set_diffs(self, diffs_dict): + self.oib_diffs = dict(sorted(diffs_dict.items())) + + def set_dbldiffs(self, diffs_dict): + self.dbl_diffs = diffs_dict + + def get_dbldiffs(self): + return self.dbl_diffs + + def set_centers(self, centers): + self.bin_centers = centers + + def get_centers(self): + return self.bin_centers + + def set_edges(self, edges): + self.bin_edges = edges + + def get_edges(self): + return self.bin_edges + + def set_area(self, area): + self.bin_area = area + + def get_area(self): + return self.bin_area + + def get_name(self): + return self.name + + def get_diff_inds_map(self): + return self.diff_inds_map + + def _rgi6torgi7id(self, debug=False): + """ + return RGI version 7 glacier id for a given RGI version 6 id + + """ + self.rgi6id = self.rgi6id.split('.')[0].zfill(2) + '.' + self.rgi6id.split('.')[1] + df_sub = self.rgi7_6_df.loc[self.rgi7_6_df['rgi6_id'] == self.rgi6id, :] + + if len(df_sub) == 1: + self.rgi7id = df_sub.iloc[0]['rgi7_id'] + if debug: + print(f'RGI6:{self.rgi6id} -> RGI7:{self.rgi7id}') + elif len(df_sub) == 0: + raise IndexError(f'No matching RGI7Id for {self.rgi6id}') + elif len(df_sub) > 1: + self.rgi7id = df_sub.sort_values(by='rgi6_area_fraction', ascending=False).iloc[0]['rgi7_id'] + + def _rgi7torgi6id(self, debug=False): + """ + return RGI version 6 glacier id for a given RGI version 7 id + + """ + self.rgi7id = self.rgi7id.split('-')[0].zfill(2) + '-' + self.rgi7id.split('-')[1] + df_sub = self.rgi7_6_df.loc[self.rgi7_6_df['rgi7_id'] == self.rgi7id, :] + if len(df_sub) == 1: + self.rgi6id = df_sub.iloc[0]['rgi6_id'] + if debug: + print(f'RGI7:{self.rgi7id} -> RGI6:{self.rgi6id}') + elif len(df_sub) == 0: + raise IndexError(f'No matching RGI6Id for {self.rgi7id}') + elif len(df_sub) > 1: + self.rgi6id = df_sub.sort_values(by='rgi7_area_fraction', ascending=False).iloc[0]['rgi6_id'] + + def load(self): + """ + load Operation IceBridge data + """ + oib_fpath = glob.glob(f'{self.oib_datpath}/diffstats5_*{self.rgi7id}*.json') + if len(oib_fpath) == 0: + return + else: + oib_fpath = oib_fpath[0] + # load diffstats file + with open(oib_fpath, 'rb') as f: + self.oib_dict = json.load(f) + self.name = _split_by_uppercase(self.oib_dict['glacier_shortname']) + + def set_diff_inds_map(self, dates_table): + """ + Store mapping of date pairs in deltah['dates'] to their indices in dates_table. + """ + # create a dictionary mapping datetime values to their indices + index_map = {value: idx for idx, value in enumerate(dates_table.date.tolist())} + # map each date pair in deltah['dates'] to their indices, if not found store (None,None) + self.diff_inds_map = [ + (index_map.get(val1), index_map.get(val2)) if val1 in index_map and val2 in index_map else (None, None) + for val1, val2 in self.dbl_diffs['dates'] + ] + + def parsediffs(self, debug=False): + """ + parse COP30-relative elevation differences + """ + # get seasons stored in oib diffs + seasons = list(set(self.oib_dict.keys()).intersection(['march', 'may', 'august'])) + diffs_dict = {} + for ssn in seasons: + for yr in list(self.oib_dict[ssn].keys()): + # get survey date + doy_int = int(np.ceil(self.oib_dict[ssn][yr]['mean_doy'])) + dt_obj = datetime.datetime.strptime(f'{int(yr)}-{doy_int}', '%Y-%j') + # get survey data and filter by pixel count + diffs = np.asarray(self.oib_dict[ssn][yr]['bin_vals']['bin_median_diffs_vec']) + counts = np.asarray(self.oib_dict[ssn][yr]['bin_vals']['bin_count_vec']) + # uncertainty represented by IQR + sigmas = np.asarray(self.oib_dict[ssn][yr]['bin_vals']['bin_interquartile_range_diffs_vec']) + # add [diffs, sigma, counts] to master dictionary + diffs_dict[_round_to_nearest_month(dt_obj)] = [diffs, sigmas, counts] + # Sort the dictionary by date keys + self.set_diffs(diffs_dict) + + if debug: + print( + f'OIB survey dates:\n{", ".join([str(dt.year) + "-" + str(dt.month) + "-" + str(dt.day) for dt in list(self.oib_diffs.keys())])}' + ) + # get bin centers + self.set_centers( + ( + np.asarray(self.oib_dict[ssn][list(self.oib_dict[ssn].keys())[0]]['bin_vals']['bin_start_vec']) + + np.asarray(self.oib_dict[ssn][list(self.oib_dict[ssn].keys())[0]]['bin_vals']['bin_stop_vec']) + ) + / 2 + ) + self.set_area(np.asarray(self.oib_dict['aad_dict']['hist_bin_areas_m2'])) + edges = list(self.oib_dict[ssn][list(self.oib_dict[ssn].keys())[0]]['bin_vals']['bin_start_vec']) + edges.append(self.oib_dict[ssn][list(self.oib_dict[ssn].keys())[0]]['bin_vals']['bin_stop_vec'][-1]) + self.set_edges(np.asarray(edges)) + + def terminus_mask(self, debug=False, inplace=False): + """ + create mask of missing terminus ice using the last OIB survey. + + parameters: + - debug: bool, whether to plot debug information. + - inplace: bool, whether to modify in place. + """ + diffs = self.get_diffs() + x = self.get_centers() + oib_diffs_masked = {} + survey_dates = list(diffs.keys()) + inds = list(range(len(survey_dates)))[::-1] # reverse index list + diffs_arr = [v[0] for v in diffs.values()] # extract first array from lists + # find the lowest bin where area is nonzero + lowest_bin = np.nonzero(self.get_area())[0][0] + x = x[lowest_bin : lowest_bin + 50] + idx = None + mask = [] + + try: + for i in inds: + tmp = diffs_arr[i][lowest_bin : lowest_bin + 50] + + if np.isnan(tmp).all(): + continue # skip if all NaN + else: + # interpolate over ant nans and then find peak/trouch + goodmask = ~np.isnan(tmp) + tmp = np.interp(x, x[goodmask], tmp[goodmask]) + # identify peak/trough based on survey year + if survey_dates[i].year > 2013: + idx = np.nanargmin(tmp) + lowest_bin # look for a trough + else: + idx = np.nanargmax(-tmp) + lowest_bin # look for a peak + mask = np.arange(0, idx + 1, 1) # create mask range + break # stop once the first valid index is found + if debug: + plt.figure() + cmap = plt.cm.rainbow(np.linspace(0, 1, len(inds))) + for i in inds[::-1]: + plt.plot( + diffs_arr[i], + label=f'{survey_dates[i].year}:{survey_dates[i].month}:{survey_dates[i].day}', + c=cmap[i], + ) + if idx is not None: + plt.axvline(idx, c='k', ls=':') + plt.legend(loc='upper right') + plt.show() + + except Exception as err: + if debug: + print(f'_terminus_mask error: {err}') + mask = [] + + # apply mask while preserving list structure + for k, v in diffs.items(): + v_list = [arr.copy().astype(float) if isinstance(arr, np.ndarray) else arr for arr in v] + for i in range(len(v_list)): + if isinstance(v_list[i], np.ndarray): + v_list[i][mask] = np.nan # apply mask + + oib_diffs_masked[k] = v_list # store modified list + + if inplace: + self.set_diffs(oib_diffs_masked) + else: + return dict(sorted(oib_diffs_masked.items())) + + def get_dhdt(self): + """ + compute thinning rate per year + """ + # get nyears between surveys for averaging - round to nearest year + self.dbl_diffs['nyears'] = np.array( + [round((t[1] - t[0]).total_seconds() / (365.25 * 24 * 60 * 60)) for t in self.dbl_diffs['dates']] + ).astype(float) + # mask any diffs where nyears < 1 (shouldn't ever be the case anyways, but just in case) + self.dbl_diffs['nyears'][self.dbl_diffs['nyears'] < 1] = np.nan + # get annual averages + self.dbl_diffs['dhdt'] = self.dbl_diffs['dh'] / self.dbl_diffs['nyears'] + + def surge_mask(self, ela=0, threshold=10, inplace=False): + """ + mask surges based on some maximum thinning rate threshold below the ELA. + this simply masks an entire survey if the maximum thinning rate below the ELA is above the threshold value. + + parameters: + - ela: float, equilibrium line altitude + - threshold: float, maximum thinning rate (m/yr) below the ELA + - inplace: bool, whether to modify in place + """ + # instantiate masked dbl diffs dictionary + oib_dbl_diffs_masked = {} + # check if dhdt computed + if 'dhdt' not in self.get_dbldiffs().keys(): + self.get_dhdt() + # get dbl diffs + dbl_diffs = self.get_dbldiffs() + # get elevation values + centers = self.get_centers() + # ablation area mask + abl_mask = centers < ela # boolean mask + # identify columns (survey pairs) to mask + cols2mask = (dbl_diffs['dhdt'][abl_mask] > threshold).any(axis=0) + # retain only non-masked survey pairs + oib_dbl_diffs_masked['dates'] = [dt for i, dt in enumerate(dbl_diffs['dates']) if not cols2mask[i]] + oib_dbl_diffs_masked['nyears'] = [y for i, y in enumerate(dbl_diffs['nyears']) if not cols2mask[i]] + oib_dbl_diffs_masked['dh'] = dbl_diffs['dh'][:, ~cols2mask] + oib_dbl_diffs_masked['dhdt'] = dbl_diffs['dhdt'][:, ~cols2mask] + oib_dbl_diffs_masked['sigma'] = dbl_diffs['sigma'][:, ~cols2mask] + + # apply changes in-place or return results + if inplace: + self.set_dbldiffs(oib_dbl_diffs_masked) + + else: + return oib_dbl_diffs_masked + + def rebin(self, agg=100, inplace=False): + """ + rebin to specified bin sizes. + + parameters: + - agg: int, bin size + - inplace: bool, whether to modify in place + """ + oib_diffs_rebin = {} + + centers = self.get_centers() + edges = self.get_edges() + + # define new coarser edges (e.g., 0, 100, 200, …) + start = np.floor(edges[0] / agg) * agg + end = np.ceil(edges[-1] / agg) * agg + new_edges = np.arange(start, end + agg, agg) + + # suppress warnings for NaN-related operations + with warnings.catch_warnings(): + warnings.simplefilter('ignore', category=RuntimeWarning) + + for i, (k, v) in enumerate(self.get_diffs().items()): + # Eensure v is a list of three NumPy arrays + if not (isinstance(v, list) and len(v) == 3 and all(isinstance(arr, np.ndarray) for arr in v)): + raise ValueError(f"Expected list of 3 NumPy arrays for key '{k}', but got {v} of type {type(v)}") + + # perform binning + if i == 0: + y = stats.binned_statistic(x=centers, values=v[0], statistic=np.nanmedian, bins=new_edges)[0] + else: + y = stats.binned_statistic(x=centers, values=v[0], statistic=np.nanmedian, bins=new_edges)[0] + + s = stats.binned_statistic(x=centers, values=v[1], statistic=np.nanmedian, bins=new_edges)[0] + c = stats.binned_statistic(x=centers, values=v[2], statistic=np.nanmedian, bins=new_edges)[0] + + # store results + oib_diffs_rebin[k] = [y, s, c] + + # compute binned area + area = stats.binned_statistic( + x=centers, + values=self.get_area(), + statistic=np.nanmedian, + bins=new_edges, + )[0] + + # compute new bin centers + centers = (new_edges[:-1] + new_edges[1:]) / 2 + + # apply changes in-place or return results + if inplace: + self.set_diffs(oib_diffs_rebin) + self.set_edges(new_edges) + self.set_centers(centers) + self.set_area(area) + else: + return oib_diffs_rebin, new_edges, centers, area + + # double difference all oib diffs from the same season 1+ year apart + def dbl_diff(self, tolerance_months=0): + # prepopulate dbl_diffs dictionary object will structure with dates, dh, sigma + # where dates is a tuple for each double differenced array in the format of (date1,date2), + # where date1's cop30 differences were subtracted from date2's to get the dh values for that time span, + # and the sigma was taken as the mean sigma from each date + self.dbl_diffs['dates'] = [] + self.dbl_diffs['dh'] = [] + self.dbl_diffs['sigma'] = [] + + # convert keys to a sorted list + sorted_dates = list(self.oib_diffs.keys()) + # iterate through sorted dates + for i, date1 in enumerate(sorted_dates[:-1]): + for j in range(i + 1, len(sorted_dates)): + date2 = sorted_dates[j] + delta_mon = date2.month - date1.month + delta_mon = ((date2.year - date1.year) * 12) + delta_mon + # calculate the modulus to find how far the difference is from the closest multiple of 12 + rem = abs(delta_mon % 12) + # check if the difference is approximately an integer multiple of years ± n month + if rem <= tolerance_months or rem >= 12 - tolerance_months: + self.dbl_diffs['dates'].append((date1, date2)) + # self.dbl_diffs['dh'].append((self.oib_diffs[date2][0] - self.oib_diffs[date1][0]) / round(delta_mon/12)) + self.dbl_diffs['dh'].append(self.oib_diffs[date2][0] - self.oib_diffs[date1][0]) + # self.dbl_diffs['sigma'].append(np.sqrt((self.oib_diffs[date2][1])**2 + (self.oib_diffs[date1][1])**2)) + self.dbl_diffs['sigma'].append(self.oib_diffs[date2][1] + self.oib_diffs[date1][1]) + break # Stop looking for further matches for date1 + + # column stack dh and sigmas into single 2d array + if len(self.dbl_diffs['dh']) > 0: + self.dbl_diffs['dh'] = np.column_stack(self.dbl_diffs['dh']) + self.dbl_diffs['sigma'] = np.column_stack(self.dbl_diffs['sigma']) + # get rid of any all-nan dbl-diffs (where cop30 offsets may not have overlapped) + mask = ~np.all(np.isnan(self.dbl_diffs['dh']), axis=0) + self.dbl_diffs['dates'] = [val for val, keep in zip(self.dbl_diffs['dates'], mask) if keep] + self.dbl_diffs['dh'] = self.dbl_diffs['dh'][:, mask] + self.dbl_diffs['sigma'] = self.dbl_diffs['sigma'][:, mask] + + # check if deltah is all nan + if np.isnan(self.dbl_diffs['dh']).all(): + self.dbl_diffs['dh'] = None + self.dbl_diffs['sigma'] = None + + def filter_on_pixel_count(self, pctl=15, inplace=False): + """ + filter oib diffs by perntile pixel count + + parameters: + - pctl: int, percentile + - inplace: bool, whether to modify in place + """ + oib_diffs_filt = {} + for k, v in self.get_diffs().items(): + arr = v[2].astype(float) # convert 2nd tuple element (count) to float + arr[arr == 0] = np.nan # replace any 0 counts to nan + mask = arr < np.nanpercentile(arr, pctl) + v_list = list(v) + # Apply mask only to numpy arrays + for i in range(len(v_list)): + if isinstance(v_list[i], np.ndarray): + v_list[i] = v_list[i].copy().astype(float) # ensure modification doesn't affect original + v_list[i][mask] = np.nan # apply mask + + # set key with updated list of arrays + oib_diffs_filt[k] = v_list + + if inplace: + self.set_diffs(oib_diffs_filt) + else: + return oib_diffs_filt + + def remove_outliers_zscore(self, zscore=3, inplace=False): + """ + z-score filter based on sigma-obs + + parameters: + - zscore: int, z-score + - inplace: bool, whether to modify in place + """ + oib_diffs_filt = {} + + for k, v in self.get_diffs().items(): + arr = v[1].astype(float) # convert sigma-obs to float + if not np.isnan(arr).all(): + mean = np.nanmean(arr) + std = np.nanstd(arr) + else: + mean = np.nan + std = np.nan + + # avoid division by zero + if std == 0 or np.isnan(std): + mask = np.full(arr.shape, False) # no outliers if std is zero + else: + mask = np.abs((arr - mean) / std) >= zscore # boolean mask + + v_list = list(v) + for i in range(len(v_list)): + if isinstance(v_list[i], np.ndarray) and np.any(mask): + v_list[i] = v_list[i].copy().astype(float) # copy only if we modify it + v_list[i][mask] = np.nan # apply NaN mask + + # set key with updated list of arrays + oib_diffs_filt[k] = v_list + + if inplace: + self.set_diffs(oib_diffs_filt) + else: + return oib_diffs_filt + + def save_elevchange1d( + self, + outdir=f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["elev_change_1d"]["elev_change_1d_relpath"]}', + csv=False, + ): + """ + Save elevation change data in a format compatible with elevchange1d module. + + Parameters + ---------- + outdir : str + Directory to save the elevation change data. + + format will be a JSON file with the following structure: + { + 'ref_dem': str, + 'ref_dem_year': int, + 'dates': [(period1_start, period1_end), (period2_start, period2_end), ... (periodM_start, periodM_end)], + 'bin_edges': [edge0, edge1, ..., edgeN], + 'bin_area': [area0, area1, ..., areaN-1], + 'dh': [[dh_bin1_period1, dh_bin2_period1, ..., dh_binN-1_period1], + [dh_bin1_period2, dh_bin2_period2, ..., dh_binN-1_period2], + ... + [dh_bin1_periodM, dh_bin2_periodM, ..., dh_binN-1_periodM]], + 'dh_sigma': [[sigma_bin1_period1, sigma_bin2_period1, ..., sigma_binN-1_period1], + [sigma_bin1_period2, sigma_bin2_period2, ..., sigma_binN-1_period2], + ... + [sigma_bin1_periodM, sigma_bin2_periodM, ..., sigma_binN-1_periodM]], + } + note: 'ref_dem' is the reference DEM used for elevation-binning. + 'ref_dem_year' is the acquisition year of the reference DEM. + 'dates' are tuples (or length-2 sublists) of the start and stop date of an individual elevation change record + and are stored as strings in 'YYYY-MM-DD' format. + 'bin_edges' should be a list length N containing the elevation values of each bin edge. + 'bin_area' should be a list of length N-1 containing the bin areas given by the 'ref_dem' (optional). + 'dh' should be M lists of length N-1, where M is the number of date pairs and N is the number of bin edges. + 'dh_sigma' should eaither be M lists of shape N-1 a scalar value. + + """ + # Ensure output directory exists + os.makedirs(outdir, exist_ok=True) + # Prepare data for saving + elev_change_data = { + 'ref_dem': 'COP30', + 'ref_dem_year': 2013, # hardcoded for now since all OIB diffs are relative to COP30 (2013) + 'dates': [(dt[0].strftime('%Y-%m-%d'), dt[1].strftime('%Y-%m-%d')) for dt in self.dbl_diffs['dates']], + 'bin_edges': self.bin_edges.tolist(), + 'bin_area': self.bin_area.tolist(), + 'dh': self.dbl_diffs['dh'].T.tolist(), + 'dh_sigma': self.dbl_diffs['sigma'].T.tolist(), + } + + # Save to JSON file + outfp = os.path.join(outdir, f'{self.rgi6id}_elev_change_1d.json') + with open(outfp, 'w') as f: + json.dump(elev_change_data, f) + + if csv: + edges = np.array(elev_change_data['bin_edges']) + ref_dem = elev_change_data['ref_dem'] + ref_year = elev_change_data['ref_dem_year'] + bin_area = np.array(elev_change_data.get('bin_area', [np.nan] * (len(edges) - 1))) + + records = [] + for period_idx, (start, end) in enumerate(elev_change_data['dates']): + dh_vals = np.array(elev_change_data['dh'][period_idx]) + dh_sig = np.array(elev_change_data['dh_sigma'][period_idx]) + for i in range(len(edges) - 1): + records.append( + { + 'bin_start': edges[i], + 'bin_stop': edges[i + 1], + 'bin_area': bin_area[i], + 'date_start': start, + 'date_end': end, + 'dh': dh_vals[i], + 'dh_sigma': dh_sig[i], + 'ref_dem': ref_dem, + 'ref_dem_year': ref_year, + } + ) + + df = pd.DataFrame(records) + outfp = os.path.join(outdir, f'{self.rgi6id}_elev_change_1d.csv') + df.to_csv(outfp, index=False) + + +def _split_by_uppercase(text): + """Add space before each uppercase letter (except at the start of the string.""" + return re.sub(r'(?= 15: + # Round up to the first day of next month + next_month = dt.replace(day=1) + timedelta(days=32) + return next_month.replace(day=1) + else: + # Round down to the first day of the current month + return dt.replace(day=1) + + ### not fully working yet ### + # def _savgol_smoother(self, window=5, poly=2, inplace=False): + # """ + # smooths an array using Savitzky-Golay filter with NaN handling + + # parameters: + # - window: int, window size + # - poly: int, polynomial degree + # - inplace: bool, whether to modify in place + # """ + # oib_diffs_filt = {} + # x = self.get_centers() + + # for k, v in self.get_diffs().items(): + # filtered_data = [] + # for i in range(2): # apply filtering to both v[0] and v[1] + # arr = v[i].astype(float) # convert to float + # mask = np.isnan(arr) + # data_filled = np.copy(arr) + # # interpolate NaNs + # data_filled[mask] = np.interp(x[mask], x[~mask], arr[~mask]) + # # apply Savitzky-Golay filter + # smoothed = signal.savgol_filter(data_filled, window_length=window, polyorder=poly) + # # restore NaNs + # smoothed[mask] = np.nan + # filtered_data.append(smoothed) + + # # populate filtered dictionary with smoothed data + # oib_diffs_filt[k] = [filtered_data[0], filtered_data[1], v[2]] + + # if inplace: + # self.set_diffs(oib_diffs_filt) + # else: + # return oib_diffs_filt diff --git a/pygem/shop/mbdata.py b/pygem/shop/mbdata.py index 9b23897b..1ff56333 100755 --- a/pygem/shop/mbdata.py +++ b/pygem/shop/mbdata.py @@ -7,7 +7,6 @@ """ # Built-in libaries -import json import logging import os @@ -60,23 +59,14 @@ def mb_df_to_gdir( """ # get dataset name (could potentially be swapped with others besides Hugonnet21) mbdata_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}' - mbdata_fp_fa = ( - mbdata_fp - + pygem_prms['calib']['data']['massbalance']['hugonnet2021_facorrected_fn'] - ) + mbdata_fp_fa = mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_facorrected_fn'] if facorrected and os.path.exists(mbdata_fp_fa): mbdata_fp = mbdata_fp_fa else: - mbdata_fp = ( - mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_fn'] - ) + mbdata_fp = mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_fn'] - assert os.path.exists(mbdata_fp), ( - 'Error, mass balance dataset does not exist: {mbdata_fp}' - ) - assert 'hugonnet2021' in mbdata_fp.lower(), ( - 'Error, mass balance dataset not yet supported: {mbdata_fp}' - ) + assert os.path.exists(mbdata_fp), 'Error, mass balance dataset does not exist: {mbdata_fp}' + assert 'hugonnet2021' in mbdata_fp.lower(), 'Error, mass balance dataset not yet supported: {mbdata_fp}' rgiid_cn = 'rgiid' mb_cn = 'mb_mwea' mberr_cn = 'mb_mwea_err' @@ -117,21 +107,16 @@ def mb_df_to_gdir( for key, value in { 'mb_mwea': float(mb_mwea), 'mb_mwea_err': float(mb_mwea_err), - 'mb_clim_mwea': float(mb_clim_mwea) - if mb_clim_mwea is not None - else None, - 'mb_clim_mwea_err': float(mb_clim_mwea_err) - if mb_clim_mwea_err is not None - else None, + 'mb_clim_mwea': float(mb_clim_mwea) if mb_clim_mwea is not None else None, + 'mb_clim_mwea_err': float(mb_clim_mwea_err) if mb_clim_mwea_err is not None else None, 't1_str': t1_str, 't2_str': t2_str, 'nyears': nyears, }.items() if value is not None } - mb_fn = gdir.get_filepath('mb_calib_pygem') - with open(mb_fn, 'w') as f: - json.dump(mbdata, f) + + gdir.write_json(mbdata, 'mb_calib_pygem') # @entity_task(log, writes=['mb_obs']) diff --git a/pygem/shop/meltextent_and_snowline_1d.py b/pygem/shop/meltextent_and_snowline_1d.py index 3fec0cb0..5d4f418b 100644 --- a/pygem/shop/meltextent_and_snowline_1d.py +++ b/pygem/shop/meltextent_and_snowline_1d.py @@ -108,9 +108,7 @@ def validate_meltextent_1d_structure(data): try: datetime.datetime.strptime(date_str, '%Y-%m-%d') except ValueError: - raise ValueError( - f"Invalid date format in 'dates[{i}]': {date_str}" - ) from None + raise ValueError(f"Invalid date format in 'dates[{i}]': {date_str}") from None # Validate z z = data['z'] @@ -143,21 +141,14 @@ def validate_meltextent_1d_structure(data): # Validate reference DEM ref_dem = data['ref_dem'].dropna().unique() if not isinstance(ref_dem, (str)): - raise TypeError( - f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__})." - ) + raise TypeError(f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__}).") # Validate reference DEM year dem_year = data['ref_dem_year'].dropna().unique() if len(dem_year) != 1: - raise ValueError( - f"'ref_dem_year' must have exactly one unique value, " - f'but found {len(dem_year)}: {dem_year}' - ) + raise ValueError(f"'ref_dem_year' must have exactly one unique value, but found {len(dem_year)}: {dem_year}") if not isinstance(dem_year, (int)): - raise TypeError( - f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__})." - ) + raise TypeError(f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__}).") return True @@ -249,9 +240,7 @@ def validate_snowline_1d_structure(data): try: datetime.datetime.strptime(date_str, '%Y-%m-%d') except ValueError: - raise ValueError( - f"Invalid date format in 'dates[{i}]': {date_str}" - ) from None + raise ValueError(f"Invalid date format in 'dates[{i}]': {date_str}") from None # Validate z z = data['z'] @@ -284,21 +273,14 @@ def validate_snowline_1d_structure(data): # Validate reference DEM ref_dem = data['ref_dem'].dropna().unique() if not isinstance(ref_dem, (str)): - raise TypeError( - f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__})." - ) + raise TypeError(f"'ref_dem' must be an string, but got {ref_dem} ({type(ref_dem).__name__}).") # Validate reference DEM year dem_year = data['ref_dem_year'].dropna().unique() if len(dem_year) != 1: - raise ValueError( - f"'ref_dem_year' must have exactly one unique value, " - f'but found {len(dem_year)}: {dem_year}' - ) + raise ValueError(f"'ref_dem_year' must have exactly one unique value, but found {len(dem_year)}: {dem_year}") if not isinstance(dem_year, (int)): - raise TypeError( - f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__})." - ) + raise TypeError(f"'ref_dem_year' must be an integer, but got {dem_year} ({type(dem_year).__name__}).") return True diff --git a/pygem/shop/oib.py b/pygem/shop/oib.py deleted file mode 100644 index a35ce02d..00000000 --- a/pygem/shop/oib.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2024 Brandon Tober , David Rounce - -Distributed under the MIT license - -NASA Operation IceBridge data and processing class -""" - -import datetime -import glob -import json -import re -import warnings - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from scipy import stats - -from pygem.setup.config import ConfigManager - -# instantiate ConfigManager -config_manager = ConfigManager() -# read the config -pygem_prms = config_manager.read_config() - - -class oib: - def __init__(self, rgi6id='', rgi7id=''): - self.oib_datpath = ( - f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["oib"]["oib_relpath"]}' - ) - self.rgi7_6_df = pd.read_csv(f'{self.oib_datpath}/../oibak_rgi6_rgi7_ids.csv') - self.rgi7_6_df['rgi7id'] = ( - self.rgi7_6_df['rgi7id'].str.split('RGI2000-v7.0-G-').str[1] - ) - self.rgi7_6_df['rgi6id'] = self.rgi7_6_df['rgi6id'].str.split('RGI60-').str[1] - self.rgi6id = rgi6id - self.rgi7id = rgi7id - self.name = None - # instatntiate dictionary to hold all data - store the data by survey date, with each key containing a tuple with the binned differences and uncertainties (diffs, sigma) - self.oib_diffs = {} - self.dbl_diffs = {} - self.bin_edges = None - self.bin_centers = None - self.bin_area = None - - def _get_diffs(self): - return self.oib_diffs - - def _get_dbldiffs(self): - return self.dbl_diffs - - def _get_centers(self): - return self.bin_centers - - def _get_edges(self): - return self.bin_edges - - def _get_area(self): - return self.bin_area - - def _get_name(self): - return self.name - - def _rgi6torgi7id(self, debug=False): - """ - return RGI version 7 glacier id for a given RGI version 6 id - - """ - self.rgi6id = ( - self.rgi6id.split('.')[0].zfill(2) + '.' + self.rgi6id.split('.')[1] - ) - # rgi7id = self.rgi7_6_df.loc[lambda self.rgi7_6_df: self.rgi7_6_df['rgi6id'] == rgi6id,'rgi7id'].tolist() - rgi7id = self.rgi7_6_df.loc[ - self.rgi7_6_df['rgi6id'] == self.rgi6id, 'rgi7id' - ].tolist() - if len(rgi7id) == 1: - self.rgi7id = rgi7id[0] - if debug: - print(f'RGI6:{self.rgi6id} -> RGI7:{self.rgi7id}') - elif len(rgi7id) == 0: - raise IndexError(f'No matching RGI7Id for {self.rgi6id}') - elif len(rgi7id) > 1: - raise IndexError(f'More than one matching RGI7Id for {self.rgi6id}') - - def _rgi7torgi6id(self, debug=False): - """ - return RGI version 6 glacier id for a given RGI version 7 id - - """ - self.rgi7id = ( - self.rgi7id.split('-')[0].zfill(2) + '-' + self.rgi7id.split('-')[1] - ) - # rgi6id = self.rgi7_6_df.loc[lambda self.rgi7_6_df: self.rgi7_6_df['rgi7id'] == rgi7id,'rgi6id'].tolist() - rgi6id = self.rgi7_6_df.loc[ - self.rgi7_6_df['rgi7id'] == self.rgi7id, 'rgi6id' - ].tolist() - if len(rgi6id) == 1: - self.rgi6id = rgi6id[0] - if debug: - print(f'RGI7:{self.rgi7id} -> RGI6:{self.rgi6id}') - elif len(rgi6id) == 0: - raise IndexError(f'No matching RGI6Id for {self.rgi7id}') - elif len(rgi6id) > 1: - raise IndexError(f'More than one matching RGI6Id for {self.rgi7id}') - - def _date_check(self, dt_obj): - """ - if survey date in given month 0 - lowest_bin = np.where(np.asarray(self.bin_area) != 0)[0][0] - idx = None - mask = [] - try: - for i in inds: - tmp = diffs[i][lowest_bin : lowest_bin + 50] - if np.isnan(tmp).all(): - continue - else: - # find peak we'll bake in the assumption that terminus thickness has decreased over time - we'll thus look for a trough if yr>=2013 (cop30 date) - if survey_dates[i].year > 2013: - idx = np.nanargmin(tmp) + lowest_bin - else: - tmp = -1 * tmp - idx = np.nanargmax(tmp) + lowest_bin - mask = np.arange(0, idx + 1, 1) - break - if debug: - plt.figure() - cmap = plt.cm.rainbow(np.linspace(0, 1, len(inds))) - for i in inds[::-1]: - plt.plot( - diffs[i], - label=f'{survey_dates[i].year}:{survey_dates[i].month}:{survey_dates[i].day}', - c=cmap[i], - ) - if idx: - plt.axvline(idx, c='k', ls=':') - plt.legend(loc='upper right') - plt.show() - - except Exception as err: - if debug: - print(f'_filter_terminus_missing_ice error: {err}') - mask = [] - - # apply mask - for tup in self.oib_diffs.values(): - tup[0][mask] = np.nan - tup[1][mask] = np.nan - - def _rebin(self, agg=100): - if agg: - # aggregate both model and obs to specified size m bins - nbins = int(np.ceil((self.bin_centers[-1] - self.bin_centers[0]) // agg)) - with warnings.catch_warnings(): - warnings.filterwarnings('ignore') - for i, (k, tup) in enumerate(self.oib_diffs.items()): - if i == 0: - y, self.bin_edges, _ = stats.binned_statistic( - x=self.bin_centers, - values=tup[0], - statistic=np.nanmean, - bins=nbins, - ) - else: - y = stats.binned_statistic( - x=self.bin_centers, - values=tup[0], - statistic=np.nanmean, - bins=self.bin_edges, - )[0] - s = stats.binned_statistic( - x=self.bin_centers, - values=tup[1], - statistic=np.nanmean, - bins=self.bin_edges, - )[0] - self.oib_diffs[k] = (y, s) - self.bin_area = stats.binned_statistic( - x=self.bin_centers, - values=self.bin_area, - statistic=np.nanmean, - bins=self.bin_edges, - )[0] - self.bin_centers = (self.bin_edges[:-1] + self.bin_edges[1:]) / 2 - - # double difference all oib diffs from the same season 1+ year apart - def _dbl_diff(self, months=range(1, 13)): - # prepopulate dbl_diffs dictionary object will structure with dates, dh, sigma - # where dates is a tuple for each double differenced array in the format of (date1,date2), - # where date1's cop30 differences were subtracted from date2's to get the dh values for that time span, - # and the sigma was taken as the mean sigma from each date - self.dbl_diffs['dates'] = [] - self.dbl_diffs['dh'] = [] - self.dbl_diffs['sigma'] = [] - # loop through months - for m in months: - # filter and sort dates to include only those in the target month - filtered_dates = sorted( - [x for x in list(self.oib_diffs.keys()) if x.month == m] - ) - # Calculate differences for consecutive pairs that are >=1 full year apart - for i in range(len(filtered_dates) - 1): - date1 = filtered_dates[i] - date2 = filtered_dates[i + 1] - year_diff = date2.year - date1.year - - # Check if the pair is at least one full year apart - if year_diff >= 1: - self.dbl_diffs['dates'].append((date1, date2)) - self.dbl_diffs['dh'].append( - self.oib_diffs[date2][0] - self.oib_diffs[date1][0] - ) - # self.dbl_diffs['sigma'].append((self.oib_diffs[date2][1] + self.oib_diffs[date1][1]) / 2) - self.dbl_diffs['sigma'].append( - self.oib_diffs[date2][1] + self.oib_diffs[date1][1] - ) - # column stack dh and sigmas into single 2d array - if len(self.dbl_diffs['dh']) > 0: - self.dbl_diffs['dh'] = np.column_stack(self.dbl_diffs['dh']) - self.dbl_diffs['sigma'] = np.column_stack(self.dbl_diffs['sigma']) - else: - self.dbl_diffs['dh'] = np.nan - # check if deltah is all nan - if np.isnan(self.dbl_diffs['dh']).all(): - self.dbl_diffs['dh'] = None - self.dbl_diffs['sigma'] = None - - def _elevchange_to_masschange( - self, - ela, - density_ablation=pygem_prms['constants']['density_ice'], - density_accumulation=700, - ): - # convert elevation changes to mass change using piecewise density conversion - if self.dbl_diffs['dh'] is not None: - # populate density conversion column corresponding to bin center elevation - conversion_factor = np.ones(len(self.bin_centers)) - conversion_factor[np.where(self.bin_centers < ela)] = density_ablation - conversion_factor[np.where(self.bin_centers >= ela)] = density_accumulation - # get change in mass per unit area as (dz * rho) [dmass / dm2] - self.dbl_diffs['dmda'] = ( - self.dbl_diffs['dh'] * conversion_factor[:, np.newaxis] - ) - self.dbl_diffs['dmda_err'] = ( - self.dbl_diffs['sigma'] * conversion_factor[:, np.newaxis] - ) - else: - self.dbl_diffs['dmda'] = None - self._dbl_diff['dmda_err'] = None - - -def _filter_on_pixel_count(arr, pctl=15): - """ - filter oib diffs by perntile pixel count - """ - arr = arr.astype(float) - arr[arr == 0] = np.nan - mask = arr < np.nanpercentile(arr, pctl) - return mask - - -def split_by_uppercase(text): - # Add space before each uppercase letter (except at the start of the string) - return re.sub(r'(? 1 - ] + vars_to_check = [name for name, var in simds.variables.items() if len(var.dims) > 1] vars_to_check = [item for item in vars_to_check if item not in vars_to_skip] for var in vars_to_check: @@ -99,9 +95,7 @@ def test_check_compiled_product(rootdir): simvar = simds[var] comppath = os.path.join(compdir, var, '01') comppath = glob.glob(f'{comppath}/R01_{var}*.nc')[0] - assert os.path.isfile(comppath), ( - f'Compiled product not found for {var} at {comppath}' - ) + assert os.path.isfile(comppath), f'Compiled product not found for {var} at {comppath}' with xr.open_dataset(comppath) as compds: compvar = compds[var] diff --git a/pygem/utils/_funcs.py b/pygem/utils/_funcs.py index c5cd60eb..84ff15eb 100755 --- a/pygem/utils/_funcs.py +++ b/pygem/utils/_funcs.py @@ -12,6 +12,7 @@ import json import numpy as np +from scipy.interpolate import interp1d from pygem.setup.config import ConfigManager @@ -65,12 +66,7 @@ def annualweightedmean_array(var, dates_table): weights = (dayspermonth / daysperyear[:, np.newaxis]).reshape(-1) # computes weights for each element, then reshapes it from matrix (rows-years, columns-months) to an array, # where each column (each monthly timestep) is the weight given to that specific month - var_annual = ( - (var * weights[np.newaxis, :]) - .reshape(-1, 12) - .sum(axis=1) - .reshape(-1, daysperyear.shape[0]) - ) + var_annual = (var * weights[np.newaxis, :]).reshape(-1, 12).sum(axis=1).reshape(-1, daysperyear.shape[0]) # computes matrix (rows - bins, columns - year) of weighted average for each year # explanation: var*weights[np.newaxis,:] multiplies each element by its corresponding weight; .reshape(-1,12) # reshapes the matrix to only have 12 columns (1 year), so the size is (rows*cols/12, 12); .sum(axis=1) @@ -81,8 +77,7 @@ def annualweightedmean_array(var, dates_table): var_annual = var_annual.reshape(var_annual.shape[0]) elif pygem_prms['time']['timestep'] == 'daily': print( - '\nError: need to code the groupbyyearsum and groupbyyearmean for daily timestep.' - 'Exiting the model run.\n' + '\nError: need to code the groupbyyearsum and groupbyyearmean for daily timestep.Exiting the model run.\n' ) exit() return var_annual @@ -113,15 +108,58 @@ def haversine_dist(grid_lons, grid_lats, target_lons, target_lats): dlon = grid_lons - target_lons dlat = grid_lats - target_lats - a = ( - np.sin(dlat / 2.0) ** 2 - + np.cos(target_lats) * np.cos(grid_lats) * np.sin(dlon / 2.0) ** 2 - ) + a = np.sin(dlat / 2.0) ** 2 + np.cos(target_lats) * np.cos(grid_lats) * np.sin(dlon / 2.0) ** 2 c = 2 * np.arcsin(np.sqrt(a)) return R * c # (n_targets, ncol) +def interp1d_fill_gaps(x): + """ + Interpolate valid (non-NaN) values in a 1D array using linear interpolation, + without extrapolating from NaNs at the edges. + + Parameters: + ---------- + x : ndarray + A 1D array with possible NaN values to interpolate. + + Returns: + ------- + x : ndarray + The 1D array with interpolated values for the NaN entries, leaving the valid values unchanged. + + Notes: + ------ + This function assumes that the input array `x` has evenly spaced data. It interpolates within the valid range of + data and does not extrapolate beyond the first and last valid data points. + """ + # Find valid (non-NaN) indices + mask = ~np.isnan(x) + + # If there are fewer than 2 valid values, return the array as is (no interpolation possible) + if mask.sum() < 2: + return x + + # Indices of valid (non-NaN) values + valid_indices = np.where(mask)[0] + first, last = valid_indices[0], valid_indices[-1] # Boundaries for valid range + + # Create the interpolation function based on valid indices + interp_func = interp1d( + valid_indices, + x[mask], + kind='linear', + bounds_error=False, + fill_value='extrapolate', + ) + + # Interpolate only within the valid range (avoid extrapolation beyond valid indices) + x[first : last + 1] = interp_func(np.arange(first, last + 1)) + + return x + + def append_json(file_path, new_key, new_value): """ Opens a JSON file, reads its content, adds a new key-value pair, diff --git a/pyproject.toml b/pyproject.toml index 85565cb4..7ce3a44e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff] -line-length = 88 # Default +line-length = 120 [tool.ruff.format] quote-style = "single" From b1f89f7ab23ebd111e098021cb9b27b379af3621 Mon Sep 17 00:00:00 2001 From: "brandon s. tober" Date: Wed, 22 Oct 2025 12:52:25 -0400 Subject: [PATCH 07/19] Export regional Glen A from calibrated inversion --- pygem/bin/run/run_inversion.py | 140 +++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 8 deletions(-) diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index 7d60e3b5..6e6c089f 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -6,6 +6,8 @@ import numpy as np import pandas as pd +pd.set_option('display.float_format', '{:.3e}'.format) + # pygem imports from pygem.setup.config import ConfigManager @@ -13,7 +15,8 @@ config_manager = ConfigManager() # read the config pygem_prms = config_manager.read_config() -from oggm import cfg, tasks, workflow +from oggm import cfg, tasks, utils, workflow +from oggm.exceptions import InvalidWorkflowError import pygem.pygem_modelsetup as modelsetup from pygem import class_climate @@ -24,11 +27,89 @@ from pygem.shop import debris, mbdata from pygem.utils._funcs import str2bool -cfg.initialize() +cfg.initialize(logging_level=pygem_prms['oggm']['logging_level']) cfg.PATHS['working_dir'] = f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' -def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None, reset_gdirs=False, debug=False): +def export_regional_results(regions, outpath): + # Directory containing the per-region CSVs + outdir, outname = os.path.split(outpath) + # loop through regional output dataframes + dfs = [] + filepaths_to_delete = [] + for r in regions: + # construct the filename using zero-padded format + filename = outname.replace('.csv', f'_R{str(r).zfill(2)}.csv') + filepath = os.path.join(outdir, filename) + if os.path.exists(filepath): + df = pd.read_csv(filepath) + df['rnum'] = r # for sorting later + dfs.append(df) + filepaths_to_delete.append(filepath) + else: + print(f'Warning: {filepath} not found') + + # merge all into one DataFrame + merged_df = pd.concat(dfs, ignore_index=True) + + # sort by the region number + merged_df = merged_df.sort_values('rnum').drop(columns='rnum') + + # if the file already exists, replace rows with same '01Region' + if os.path.exists(outpath): + existing_df = pd.read_csv(outpath) + # remove rows with the same '01Region' values as in the new merge + merged_df = pd.concat( + [existing_df[~existing_df['01Region'].isin(merged_df['01Region'])], merged_df], ignore_index=True + ) + # re-sort + merged_df = merged_df.sort_values('01Region') + + # export final merged csv + merged_df.to_csv(outpath, index=False) + + # Delete individual regional files + for fp in filepaths_to_delete: + os.remove(fp) + + +def get_regional_volume(gdirs, ignore_missing=True): + """ + Calculate the modeled volume [m3] and consensus volume [m3] for the given set of glaciers + """ + # get itmix vol + # Get the ref data for the glaciers we have + df = pd.read_hdf(utils.get_demo_file('rgi62_itmix_df.h5')) + rids = [gdir.rgi_id for gdir in gdirs] + + found_ids = df.index.intersection(rids) + if not ignore_missing and (len(found_ids) != len(rids)): + raise InvalidWorkflowError( + 'Could not find matching indices in the ' + 'consensus estimate for all provided ' + 'glaciers. Set ignore_missing=True to ' + 'ignore this error.' + ) + + df = df.reindex(rids) + itmix_vol = df.sum()['vol_itmix_m3'] + model_vol = 0 + for gdir in gdirs: + model_vol += gdir.read_pickle('model_flowlines')[0].volume_m3 + return itmix_vol, model_vol + + +def run( + glac_no, + ncores=1, + calibrate_regional_glen_a=False, + glen_a=None, + fs=None, + reset_gdirs=False, + regional_inv=False, + outpath=None, + debug=False, +): """ Run OGGM's bed inversion for a list of RGI glacier IDs using PyGEM's mass balance model. """ @@ -137,6 +218,7 @@ def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None ### CALCULATE APPARENT MASS BALANCE ### ####################################### # note, PyGEMMassBalance_wrapper is passed to `tasks.apparent_mb_from_any_mb` as the `mb_model_class` so that PyGEMs mb model is used for apparent mb + # apply inversion_filter on mass balance with debris to avoid negative flux workflow.execute_entity_task( tasks.apparent_mb_from_any_mb, gdirs, @@ -144,6 +226,7 @@ def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None PyGEMMassBalance_wrapper, fl_str='inversion_flowlines', option_areaconstant=True, + inversion_filter=pygem_prms['mb']['include_debris'], ), ) # add debris data to flowlines @@ -157,7 +240,7 @@ def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None if calibrate_regional_glen_a: if debug: print("Calibrating Glen's A") - workflow.calibrate_inversion_from_consensus( + cdf = workflow.calibrate_inversion_from_consensus( gdirs, apply_fs_on_mismatch=True, error_on_mismatch=False, # if you running many glaciers some might not work @@ -165,6 +248,8 @@ def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None # the equilibrium assumption for retreating glaciers (see. Figure 5 of Maussion et al. 2019) volume_m3_reference=None, # here you could provide your own total volume estimate in m3 ) + itmix_vol = cdf.sum()['vol_itmix_m3'] + model_vol = cdf.sum()['vol_oggm_m3'] for gdir in gdirs: if calibrate_regional_glen_a: @@ -258,11 +343,40 @@ def run(glac_no, ncores=1, calibrate_regional_glen_a=False, glen_a=None, fs=None # add debris to model_flowlines workflow.execute_entity_task(debris.debris_binned, gdirs, fl_str='model_flowlines') + # get itmix and inversion cumulative volumes + if not calibrate_regional_glen_a: + itmix_vol, model_vol = get_regional_volume(gdirs) + + reg = glac_no[0].split('.')[0].zfill(2) + # prepare ouptut dataset + df = pd.Series( + { + '01Region': reg, + 'count': len(glac_no), + 'inversion_glen_a': gdirs[0].get_diagnostics()['inversion_glen_a'], + 'inversion_fs': gdirs[0].get_diagnostics()['inversion_fs'], + 'vol_itmix_m3': itmix_vol, + 'vol_model_m3': model_vol, + } + ) + + if debug: + print(df) + + # export + if outpath: + if calibrate_regional_glen_a and regional_inv: + pd.DataFrame([df]).to_csv(outpath.replace('.csv', f'_R{reg}.csv'), index=False) + else: + raise ValueError( + 'Only set up to export regional Glen A parameters if regionally calibrated against the regional ice volume estimate.' + ) + def main(): # define ArgumentParser parser = argparse.ArgumentParser( - description="Perform glacier bed inversion (defaults to find best Glen's A for each RGI order 01 region)" + description="Perform glacier bed inversion (defaults to finding best Glen's A for each RGI order 01 region)" ) # add arguments parser.add_argument( @@ -274,7 +388,6 @@ def main(): ) parser.add_argument( '-rgi_glac_number', - action='store', type=float, default=pygem_prms['setup']['glac_no'], nargs='+', @@ -283,7 +396,6 @@ def main(): ( parser.add_argument( '-rgi_glac_number_fn', - action='store', type=str, default=None, help='Filepath containing list of rgi_glac_number, helpful for running batches on spc', @@ -309,11 +421,16 @@ def main(): ) parser.add_argument( '-ncores', - action='store', type=int, default=1, help='Number of simultaneous processes (cores) to use', ) + parser.add_argument( + '-outpath', + type=str, + default=f'{pygem_prms["root"]}/{pygem_prms["sim"]["oggm_dynamics"]["glen_a_regional_relpath"]}', + help='Output datapath', + ) parser.add_argument( '-reset_gdirs', action='store_true', @@ -340,6 +457,7 @@ def main(): )['rgino_str'].values.tolist() for r01 in args.rgi_region01 ] + regional_inv = True # flag to regional inversion else: batches = None if args.rgi_glac_number: @@ -347,6 +465,7 @@ def main(): # format appropriately glac_no = [float(g) for g in glac_no] batches = [f'{g:.5f}' if g >= 10 else f'0{g:.5f}' for g in glac_no] + regional_inv = False # flag to indicate per-glacier inversion elif args.rgi_glac_number_fn is not None: with open(args.rgi_glac_number_fn, 'r') as f: batches = json.load(f) @@ -359,12 +478,17 @@ def main(): glen_a=args.glen_a, fs=args.fs, reset_gdirs=args.reset_gdirs, + regional_inv=regional_inv, + outpath=args.outpath, debug=args.debug, ) for i, batch in enumerate(batches): run_partial(batch) + if args.outpath and args.calibrate_regional_glen_a: + export_regional_results(args.rgi_region01, args.outpath) + if __name__ == '__main__': main() From 66b9bf0319b859b3772d36ae9d5567004ff75157 Mon Sep 17 00:00:00 2001 From: "brandon s. tober" Date: Wed, 22 Oct 2025 14:06:35 -0400 Subject: [PATCH 08/19] Turn off inversion filter --- pygem/bin/run/run_inversion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index 6e6c089f..ab8a029c 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -226,7 +226,6 @@ def run( PyGEMMassBalance_wrapper, fl_str='inversion_flowlines', option_areaconstant=True, - inversion_filter=pygem_prms['mb']['include_debris'], ), ) # add debris data to flowlines From 0b878293bbee13bbfd63a1e3adafa62cd1a996d0 Mon Sep 17 00:00:00 2001 From: "brandon s. tober" Date: Thu, 23 Oct 2025 09:21:29 -0400 Subject: [PATCH 09/19] Load calibrated calving_k values for dynamical calibration of tidewater glaciers --- pygem/bin/run/run_calibration.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index 180a458d..dec8ee31 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -1964,6 +1964,27 @@ def rho_constraints(**kwargs): # if running full model (no emulator), or calibrating against binned elevation change, several arguments are needed if args.option_calib_elev_change_1d: + # load calibrated calving_k values for tidewater glaciers + if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: + fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' + assert os.path.exists(fp), 'Calibrated calving dataset does not exist' + calving_df = pd.read_csv(fp) + calving_rgiids = list(calving_df.RGIId) + # Use calibrated value if individual data available + if gdir.rgi_id in calving_rgiids: + calving_idx = calving_rgiids.index(gdir.rgi_id) + calving_k = calving_df.loc[calving_idx, 'calving_k'] + # Otherwise, use region's median value + else: + calving_df['O1Region'] = [ + int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values + ] + calving_df_reg = calving_df.loc[calving_df['O1Region'] == int(gdir.rgi_id[6:8]), :] + calving_k = np.median(calving_df_reg.calving_k) + # set calving_k in config + cfg.PARAMS['use_kcalving_for_run'] = True + cfg.PARAMS['calving_k'] = calving_k + # add density priors if calibrating against binned elevation change priors['rhoabl'] = { 'type': pygem_prms['calib']['MCMC_params']['rhoabl_disttype'], From ef4a7f827022a3a48d585cb04aeeedcc6f3125d4 Mon Sep 17 00:00:00 2001 From: "brandon s. tober" Date: Thu, 23 Oct 2025 11:31:20 -0400 Subject: [PATCH 10/19] Convert to elevation change considering modeled densities --- pygem/mcmc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygem/mcmc.py b/pygem/mcmc.py index 2b6df0e8..f149b9a2 100644 --- a/pygem/mcmc.py +++ b/pygem/mcmc.py @@ -247,8 +247,8 @@ def log_likelihood(self, m): rho[~self.abl_mask] = m[4] # rhoacc rho = torch.tensor(rho) self.preds[i] = pred = ( - self.preds[i] * rho[:, np.newaxis] / pygem_prms['constants']['density_ice'] - ) # scale prediction by model density values (convert from m ice to m thickness change) + self.preds[i] * (pygem_prms['constants']['density_ice'] / rho[:, np.newaxis]) + ) # scale prediction by model density values (convert from m ice to m thickness change considering modeled density) log_likehood += log_normal_density( self.obs[i][0], # observations From 96a105d5932823254da70880a0b4ac009d6569bb Mon Sep 17 00:00:00 2001 From: btobers Date: Sun, 26 Oct 2025 15:26:26 -0400 Subject: [PATCH 11/19] Ensure spinup_start_yr < 2000 --- pygem/bin/run/run_spinup.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index 9d3d03f3..2e18a9a2 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -264,6 +264,8 @@ def run(glacno_list, mb_model_params, optimize=False, periods2try=[20], outdir=N end_dt = datetime.strptime(end, '%Y-%m-%d') gd.elev_change_1d['nyrs'].append((end_dt - start_dt).days / 365.25) gd.elev_change_1d['dhdt'] = np.column_stack(gd.elev_change_1d['dh']) / gd.elev_change_1d['nyrs'] + # define minimum spinup start year + min_start_yr = min(2000, *(int(date[:4]) for pair in gd.elev_change_1d['dates'] for date in pair)) results = {} # instantiate output dictionary fig, ax = plt.subplots(1) # instantiate figure @@ -271,6 +273,8 @@ def run(glacno_list, mb_model_params, optimize=False, periods2try=[20], outdir=N # objective function to evaluate def _objective(**kwargs): fls = run_spinup(gd, **kwargs) + if fls[0] is None: + return kwargs['spinup_period'], float('inf'), None # get true spinup period (note, if initial fails, oggm tries period/2) spinup_period_ = gd.rgi_date + 1 - fls[0].y0 @@ -310,13 +314,23 @@ def _objective(**kwargs): best_value, best_model = results[best_period] # update kwarg kwargs['spinup_period'] = best_period + # ensure spinup start year <= min_start_yr + if gd.rgi_date + 1 - best_period > min_start_yr: + kwargs['spinup_start_yr'] = min_start_yr + kwargs.pop('spinup_period') + p_, best_value, best_model = _objective(**kwargs) + results[p_] = (mismatch, model) + best_period = gd.rgi_date + 1 - min_start_yr if debug: print('All results:', {k: v[0] for k, v in results.items()}) print(f'Best spinup_period = {best_period}, mismatch = {best_value}') - # find worst - worst_period = max(results, key=lambda k: results[k][0]) + # find worst - ignore failed runs + worst_period = max( + (k for k in results if results[k][0] != float('inf')), + key=lambda k: results[k][0] + ) worst_value, worst_model = results[worst_period] ############################ From ef89bf5a2fd613e1ea95b4eb64bc854460569550 Mon Sep 17 00:00:00 2001 From: btobers Date: Sun, 26 Oct 2025 15:36:05 -0400 Subject: [PATCH 12/19] Safely load elev_change_1d --- pygem/bin/run/run_calibration.py | 45 ++++++++++++++++++-------------- pygem/bin/run/run_spinup.py | 5 +--- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index dec8ee31..9923e772 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -788,6 +788,31 @@ def run(list_packed_vars): + ' +/- ' + str(np.round(mb_obs_mwea_err, 2)) ) + # load elevation change data + if args.option_calib_elev_change_1d: + # load binned elev change obs to glacier directory + gdir.elev_change_1d = gdir.read_json('elev_change_1d') + # stack dh and dh_sigma + gdir.elev_change_1d['dh'] = np.column_stack(gdir.elev_change_1d['dh']) + gdir.elev_change_1d['dh_sigma'] = ( + np.column_stack(gdir.elev_change_1d['dh_sigma']) + if not isinstance(gdir.elev_change_1d['dh_sigma'], int) + else gdir.elev_change_1d['dh_sigma'] + ) + # get observation period indices in model date_table + # create lookup dict (timestamp → index) + date_to_index = {d: i for i, d in enumerate(gdir.dates_table['date'])} + gdir.elev_change_1d['model2obs_inds_map'] = [ + ( + date_to_index.get(pd.to_datetime(start)), + date_to_index.get(pd.to_datetime(end)), + ) + for start, end in gdir.elev_change_1d['dates'] + ] + # adjust ref_startyear base don earliest available elevation calibration data (must be <= 2000) + args.ref_startyear = min( + 2000, *(int(date[:4]) for pair in gdir.elev_change_1d['dates'] for date in pair) + ) except Exception as err: gdir.mbdata = None @@ -1996,25 +2021,7 @@ def rho_constraints(**kwargs): 'mu': float(pygem_prms['calib']['MCMC_params']['rhoaccum_mu']), 'sigma': float(pygem_prms['calib']['MCMC_params']['rhoaccum_sigma']), } - # load binned elev change obs to glacier directory - gdir.elev_change_1d = gdir.read_json('elev_change_1d') - # stack dh and dh_sigma - gdir.elev_change_1d['dh'] = np.column_stack(gdir.elev_change_1d['dh']) - gdir.elev_change_1d['dh_sigma'] = ( - np.column_stack(gdir.elev_change_1d['dh_sigma']) - if not isinstance(gdir.elev_change_1d['dh_sigma'], int) - else gdir.elev_change_1d['dh_sigma'] - ) - # get observation period indices in model date_table - # create lookup dict (timestamp → index) - date_to_index = {d: i for i, d in enumerate(gdir.dates_table['date'])} - gdir.elev_change_1d['model2obs_inds_map'] = [ - ( - date_to_index.get(pd.to_datetime(start)), - date_to_index.get(pd.to_datetime(end)), - ) - for start, end in gdir.elev_change_1d['dates'] - ] + # model equilibrium line elevation for breakpoint of accumulation and ablation area density scaling gdir.ela = tasks.compute_ela( gdir, diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index 2e18a9a2..eeffb779 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -327,10 +327,7 @@ def _objective(**kwargs): print(f'Best spinup_period = {best_period}, mismatch = {best_value}') # find worst - ignore failed runs - worst_period = max( - (k for k in results if results[k][0] != float('inf')), - key=lambda k: results[k][0] - ) + worst_period = max((k for k in results if results[k][0] != float('inf')), key=lambda k: results[k][0]) worst_value, worst_model = results[worst_period] ############################ From 64514765940abffc0145ea2913309091f318aafb Mon Sep 17 00:00:00 2001 From: btobers Date: Tue, 28 Oct 2025 12:27:18 -0400 Subject: [PATCH 13/19] More robustly sample initials for any stuck chains --- pygem/bin/run/run_calibration.py | 157 ++++++++++++++++--------------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index 9923e772..60c088da 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -809,36 +809,59 @@ def run(list_packed_vars): ) for start, end in gdir.elev_change_1d['dates'] ] - # adjust ref_startyear base don earliest available elevation calibration data (must be <= 2000) - args.ref_startyear = min( - 2000, *(int(date[:4]) for pair in gdir.elev_change_1d['dates'] for date in pair) - ) + # optionally adjust ref_startyear based on earliest available elevation calibration data (must be <= 2000) + if args.spinup: + args.ref_startyear = min( + 2000, *(int(date[:4]) for pair in gdir.elev_change_1d['dates'] for date in pair) + ) + + # load calibrated calving_k values for tidewater glaciers + if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: + fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' + assert os.path.exists(fp), 'Calibrated calving dataset does not exist' + calving_df = pd.read_csv(fp) + calving_rgiids = list(calving_df.RGIId) + # Use calibrated value if individual data available + if gdir.rgi_id in calving_rgiids: + calving_idx = calving_rgiids.index(gdir.rgi_id) + calving_k = calving_df.loc[calving_idx, 'calving_k'] + # Otherwise, use region's median value + else: + calving_df['O1Region'] = [ + int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values + ] + calving_df_reg = calving_df.loc[calving_df['O1Region'] == int(gdir.rgi_id[6:8]), :] + calving_k = np.median(calving_df_reg.calving_k) + # set calving_k in config + cfg.PARAMS['use_kcalving_for_run'] = True + cfg.PARAMS['calving_k'] = calving_k + # many tidewater glaciers need a timestep << OGGM default of 60 seconds + cfg.PARAMS['cfl_min_dt'] = 0.0001 except Exception as err: gdir.mbdata = None - # LOG FAILURE fail_fp = pygem_prms['root'] + '/Output/cal_fail/' + glacier_str.split('.')[0].zfill(2) + '/' if not os.path.exists(fail_fp): os.makedirs(fail_fp, exist_ok=True) txt_fn_fail = glacier_str + '-cal_fail.txt' with open(fail_fp + txt_fn_fail, 'w') as text_file: - text_file.write(f'Error with mass balance data: {err}') - - print('\n' + glacier_str + ' mass balance data missing. Check dataset and column names.\n') + text_file.write(f'Error loading calibration data: {err}') + + # if `args.spinup`, grab appropriate model flowlines + if args.spinup: + fls = oggm_compat.get_spinup_flowlines(gdir, y0=args.ref_startyear) + # if not `args.spinup` and calibrating elevation change, grab model flowlines + elif args.option_calib_elev_change_1d: + if not os.path.exists(gdir.get_filepath('model_flowlines')): + raise FileNotFoundError('No model flowlines found - has inversion been run?') + # ref_startyear should not be < 2000 unless spinup was run + assert args.ref_startyear >= 2000, 'Must run spinup to allow for runs starting before year 2000' + fls = gdir.read_pickle('model_flowlines') except: fls = None - # if `args.spinup`, grab appropriate model flowlines - if args.spinup: - fls = oggm_compat.get_spinup_flowlines(gdir, y0=args.ref_startyear) - # if not `args.spinup` and calibrating elevation change, grab model flowlines - elif args.option_calib_elev_change_1d: - if not os.path.exists(gdir.get_filepath('model_flowlines')): - raise FileNotFoundError('No model flowlines found - has inversion been run?') - fls = gdir.read_pickle('model_flowlines') - # ----- CALIBRATION OPTIONS ------ if (fls is not None) and (gdir.mbdata is not None) and (glacier_area.sum() > 0): modelprms = { @@ -1811,48 +1834,51 @@ def calc_mb_total_minelev(modelprms): return mb_total_minelev - def get_priors(priors): - # define distribution based on priors + def get_priors(priors_dict): + # return a list of scipy.stats distributions based on the priors_dict dists = [] - for param in ['tbias', 'kp', 'ddfsnow']: - if priors[param]['type'] == 'normal': - dist = stats.norm(loc=priors[param]['mu'], scale=priors[param]['sigma']) - elif priors[param]['type'] == 'uniform': - dist = stats.uniform( - loc=priors[param]['low'], - scale=priors[param]['high'] - priors[param]['low'], - ) - elif priors[param]['type'] == 'gamma': - dist = stats.gamma( - a=priors[param]['alpha'], - scale=1 / priors[param]['beta'], - ) - elif priors[param]['type'] == 'truncnormal': - dist = stats.truncnorm( - a=(priors[param]['low'] - priors[param]['mu']) / priors[param]['sigma'], - b=(priors[param]['high'] - priors[param]['mu']) / priors[param]['sigma'], - loc=priors[param]['mu'], - scale=priors[param]['sigma'], - ) + for param, info in priors_dict.items(): + dist_type = info['type'].lower() + + if dist_type == 'normal': + dist = stats.norm(loc=info['mu'], scale=info['sigma']) + elif dist_type == 'uniform': + dist = stats.uniform(loc=info['low'], scale=info['high'] - info['low']) + elif dist_type == 'gamma': + dist = stats.gamma(a=info['alpha'], scale=1 / info['beta']) + elif dist_type == 'truncnormal': + a = (info['low'] - info['mu']) / info['sigma'] + b = (info['high'] - info['mu']) / info['sigma'] + dist = stats.truncnorm(a=a, b=b, loc=info['mu'], scale=info['sigma']) + else: + raise ValueError(f'Unsupported distribution type: {dist_type}') dists.append(dist) return dists - def get_initials(dists, threshold=0.01, pctl=None): - if pctl: - initials = [dist.ppf(pctl) for dist in dists] - else: - # sample priors - ensure that probability of each sample > .01 - initials = None - while initials is None: - # sample from each distribution - xs = [dist.rvs() for dist in dists] - # calculate densities for each sample - ps = [dist.pdf(x) for dist, x in zip(dists, xs)] - - # Check if all densities are greater than the threshold - if all(p > threshold for p in ps): - initials = xs - return initials + def get_initials(dists, central_mass=0.95, max_tries=10000, verbose=False, pctl=None): + """ + Randomly sample initial values from a list of distributions. + """ + if pctl is not None: + if not 0 <= pctl <= 1: + raise ValueError('pctl must be between 0 and 1') + return [dist.ppf(pctl) for dist in dists] + + # default: random sampling within central probability interval + lower = (1 - central_mass) / 2 + upper = 1 - lower + + for i in range(max_tries): + xs = [dist.rvs() for dist in dists] + probs = [dist.cdf(x) for dist, x in zip(dists, xs)] + if all(lower < p < upper for p in probs): + if verbose: + print(f'Accepted on try {i + 1}: {xs}') + return xs + if verbose and i % 1000 == 0: + print(f'Try {i}: {xs} (probs={probs})') + + raise RuntimeError(f'Failed to find acceptable initials after {max_tries} draws') def mb_max(**kwargs): """Psuedo-likelihood functionto ensure glacier is not completely melted.""" @@ -1989,27 +2015,6 @@ def rho_constraints(**kwargs): # if running full model (no emulator), or calibrating against binned elevation change, several arguments are needed if args.option_calib_elev_change_1d: - # load calibrated calving_k values for tidewater glaciers - if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: - fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_relpath"]}/analysis/{pygem_prms["calib"]["data"]["frontalablation"]["frontalablation_cal_fn"]}' - assert os.path.exists(fp), 'Calibrated calving dataset does not exist' - calving_df = pd.read_csv(fp) - calving_rgiids = list(calving_df.RGIId) - # Use calibrated value if individual data available - if gdir.rgi_id in calving_rgiids: - calving_idx = calving_rgiids.index(gdir.rgi_id) - calving_k = calving_df.loc[calving_idx, 'calving_k'] - # Otherwise, use region's median value - else: - calving_df['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in calving_df.RGIId.values - ] - calving_df_reg = calving_df.loc[calving_df['O1Region'] == int(gdir.rgi_id[6:8]), :] - calving_k = np.median(calving_df_reg.calving_k) - # set calving_k in config - cfg.PARAMS['use_kcalving_for_run'] = True - cfg.PARAMS['calving_k'] = calving_k - # add density priors if calibrating against binned elevation change priors['rhoabl'] = { 'type': pygem_prms['calib']['MCMC_params']['rhoabl_disttype'], From c437c27788bff54dada0a8924d99525f53d7a7af Mon Sep 17 00:00:00 2001 From: David Rounce Date: Mon, 3 Nov 2025 20:08:44 -0500 Subject: [PATCH 14/19] Enabling daily mass balance functionality (#143) Closes #133. Closes #141. Closes #142. The man updates from this PR include: * Enabling the mass balance model to run on a daily timestep * Removal of nested try/except blocks in run_simulation.py so that if the specified dynamical option fails the model does not default to a fallback option * Storage of the dynamical option within the output netcdf file * Add test_postproc_binned_subannual_thick() to test04 * Update glacierdynamics.MassRedistributionCurveModel to work with any timestep (#147) --------- Co-authored-by: brandon s. tober --- .github/workflows/test_suite.yml | 3 +- docs/calibration_options.md | 9 +- .../postproc/postproc_binned_monthly_mass.py | 314 ----- .../postproc_binned_subannual_thick.py | 356 +++++ .../postproc/postproc_compile_simulations.py | 117 +- ...hly_mass.py => postproc_subannual_mass.py} | 127 +- pygem/bin/run/run_calibration.py | 78 +- pygem/bin/run/run_simulation.py | 794 +++++------ pygem/bin/run/run_spinup.py | 91 +- pygem/class_climate.py | 15 +- pygem/glacierdynamics.py | 36 +- pygem/massbalance.py | 1177 +++++++++-------- pygem/output.py | 217 +-- pygem/plot/graphics.py | 32 +- pygem/pygem_modelsetup.py | 62 +- pygem/setup/config.py | 146 +- pygem/setup/config.yaml | 8 +- pygem/shop/mbdata.py | 1 - pygem/tests/test_04_auxiliary.py | 87 ++ ...est_04_postproc.py => test_05_postproc.py} | 32 +- pygem/utils/_funcs.py | 12 +- pyproject.toml | 4 +- 22 files changed, 1855 insertions(+), 1863 deletions(-) delete mode 100644 pygem/bin/postproc/postproc_binned_monthly_mass.py create mode 100644 pygem/bin/postproc/postproc_binned_subannual_thick.py rename pygem/bin/postproc/{postproc_monthly_mass.py => postproc_subannual_mass.py} (52%) create mode 100644 pygem/tests/test_04_auxiliary.py rename pygem/tests/{test_04_postproc.py => test_05_postproc.py} (78%) diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 4bb4a542..2c30c1bb 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -73,4 +73,5 @@ jobs: python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_01_basics.py python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_02_config.py python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_03_notebooks.py - python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_04_postproc.py + python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_04_auxiliary.py + python3 -m pytest --cov=pygem -v --durations=20 pygem/tests/test_05_postproc.py diff --git a/docs/calibration_options.md b/docs/calibration_options.md index a004e578..fa6117fb 100644 --- a/docs/calibration_options.md +++ b/docs/calibration_options.md @@ -6,11 +6,10 @@ Several calibration options exist, which vary with respect to complexity and com | Calibration option | Overview | Reference | | :--- | :--- | :--- | -| ['HH2015'](HH2015_target) | Finds single set of parameters.
Varies in order: $f_{snow}$, $k_{p}$, $T_{bias}$ | [Huss and Hock (2015)](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) | -| ['HH2015mod'](HH2015mod_target) | Finds single set of parameters.
Varies in order: $k_{p}$, $T_{bias}$ | [Rounce et al. 2020](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) | -| ['emulator'](emulator_target) | Creates emulator for ['MCMC'](MCMC_target).
Finds single set of parameters with emulator following ['HH2015mod'](HH2015mod_target) | [Rounce et al. 2023](https://www.science.org/doi/10.1126/science.abo1324) | -| ['MCMC'](MCMC_target) | Finds multiple sets of parameters using Bayesian inference with [emulator](emulator_target).
Varies $f_{snow}$, $k_{p}$, $T_{bias}$ | [Rounce et al. 2023](https://www.science.org/doi/10.1126/science.abo1324) | -| ['MCMC_fullsim'](MCMC_target) | Finds multiple sets of parameters using Bayesian inference with full model simulations.
Varies $f_{snow}$, $k_{p}$, $T_{bias}$ | [Rounce et al. 2020](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) | +| ['HH2015'](HH2015_target) | Finds single set of parameters.
Varies in order: $f_{snow}$, $k_{p}$, $T_{bias}$ | [Huss and Hock, 2015](https://www.frontiersin.org/articles/10.3389/feart.2015.00054/full) | +| ['HH2015mod'](HH2015mod_target) | Finds single set of parameters.
Varies in order: $k_{p}$, $T_{bias}$ | [Rounce et al., 2020](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) | +| ['emulator'](emulator_target) | Creates emulator for ['MCMC'](MCMC_target).
Finds single set of parameters with emulator following ['HH2015mod'](HH2015mod_target) | [Rounce et al., 2023](https://www.science.org/doi/10.1126/science.abo1324) | +| ['MCMC'](MCMC_target) | Finds many sets of parameters using Bayesian inference. Setting `calib.MCMC_params.option_use_emulator=True` in ~/PyGEM/config.yaml will run Bayesian inference using the mass balance emulator. Setting `calib.MCMC_params.option_use_emulator=False` (or when performing dynamical calibration against elevation change data)$^*$ will run Bayesian inference calibration with full model simulations.
Varies $f_{snow}$, $k_{p}$, $T_{bias}$, (optionally $\rho_{ablation}$, $\rho_{accumulation}$)$^*$ | [Rounce et al., 2020](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); [2023](https://www.science.org/doi/10.1126/science.abo1324) | | [Future options](cal_custom_target) | Stay tuned for new options coming in 2023/2024! | | The output of each calibration is a .json file that holds a dictionary of the calibration options and the subsequent model parameters. Thus, the .json file will store several calibration options. 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: diff --git a/pygem/bin/postproc/postproc_binned_monthly_mass.py b/pygem/bin/postproc/postproc_binned_monthly_mass.py deleted file mode 100644 index b24b9bd2..00000000 --- a/pygem/bin/postproc/postproc_binned_monthly_mass.py +++ /dev/null @@ -1,314 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2024 Brandon Tober David Rounce - -Distributed under the MIT license - -derive binned monthly ice thickness and mass from PyGEM simulation -""" - -# Built-in libraries -import argparse -import collections -import glob -import multiprocessing -import os -import time - -# External libraries -import numpy as np -import xarray as xr - -# pygem imports -from pygem.setup.config import ConfigManager - -# instantiate ConfigManager -config_manager = ConfigManager() -# read the config -pygem_prms = config_manager.read_config() - - -# ----- FUNCTIONS ----- -def getparser(): - """ - Use argparse to add arguments from the command line - """ - parser = argparse.ArgumentParser(description='process monthly ice thickness for PyGEM simulation') - # add arguments - parser.add_argument( - '-simpath', - action='store', - type=str, - nargs='+', - default=None, - help='path to PyGEM binned simulation (can take multiple)', - ) - parser.add_argument( - '-binned_simdir', - action='store', - type=str, - default=None, - help='directory with binned simulations for which to process monthly thickness', - ) - parser.add_argument( - '-ncores', - action='store', - type=int, - default=1, - help='number of simultaneous processes (cores) to use', - ) - - return parser - - -def get_binned_monthly(dotb_monthly, m_annual, h_annual): - """ - funciton to calculate the monthly binned ice thickness and mass - from annual climatic mass balance and annual ice thickness products - - to determine monthlyt thickness and mass, we must account for flux divergence - this is not so straight-forward, as PyGEM accounts for ice dynamics at the - end of each model year and not on a monthly timestep. - here, monthly thickness and mass is determined assuming - the flux divergence is constant throughout the year. - - annual flux divergence is first estimated by combining the annual binned change in ice - thickness and the annual binned mass balance. then, assume flux divergence is constant - throughout the year (divide annual by 12 to get monthly flux divergence). - - monthly binned flux divergence can then be combined with - monthly binned climatic mass balance to get monthly binned change in ice thickness - - - Parameters - ---------- - dotb_monthly : float - ndarray containing the climatic mass balance for each model month computed by PyGEM - shape : [#glac, #elevbins, #months] - m_annual : float - ndarray containing the average (or median) binned ice mass computed by PyGEM - shape : [#glac, #elevbins, #years] - h_annual : float - ndarray containing the average (or median) binned ice thickness at computed by PyGEM - shape : [#glac, #elevbins, #years] - - Returns - ------- - m_monthly: float - ndarray containing the binned monthly ice mass - shape : [#glac, #elevbins, #years] - h_monthly: float - ndarray containing the binned monthly ice thickness - shape : [#glac, #elevbins, #years] - """ - ### get monthly ice thickness ### - # convert mass balance from m w.e. yr^-1 to m ice yr^-1 - dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) - assert dotb_monthly.shape[2] % 12 == 0, 'Number of months is not a multiple of 12!' - - # obtain annual mass balance rate, sum monthly for each year - dotb_annual = dotb_monthly.reshape(dotb_monthly.shape[0], dotb_monthly.shape[1], -1, 12).sum( - axis=-1 - ) # climatic mass balance [m ice a^-1] - - # compute the thickness change per year - delta_h_annual = np.diff(h_annual, axis=-1) # [m ice a^-1] (nbins, nyears-1) - - # compute flux divergence for each bin - flux_div_annual = dotb_annual - delta_h_annual # [m ice a^-1] - - ### to get monthly thickness and mass we require monthly flux divergence ### - # we'll assume the flux divergence is constant througohut the year (is this a good assumption?) - # ie. take annual values and divide by 12 - use numpy repeat to repeat values across 12 months - flux_div_monthly = np.repeat(flux_div_annual / 12, 12, axis=-1) - - # get monthly binned change in thickness - delta_h_monthly = dotb_monthly - flux_div_monthly # [m ice per month] - - # get binned monthly thickness = running thickness change + initial thickness - running_delta_h_monthly = np.cumsum(delta_h_monthly, axis=-1) - h_monthly = running_delta_h_monthly + h_annual[:, :, 0][:, :, np.newaxis] - - # convert to mass per unit area - m_spec_monthly = h_monthly * pygem_prms['constants']['density_ice'] - - ### get monthly mass ### - # note, binned monthly thickness and mass is currently per unit area - # obtaining binned monthly mass requires knowledge of binned glacier area - # we do not have monthly binned area (as glacier dynamics are performed on an annual timestep in PyGEM), - # so we'll resort to using the annual binned glacier mass and thickness in order to get to binned glacier area - ######################## - # first convert m_annual to bin_voluma_annual - v_annual = m_annual / pygem_prms['constants']['density_ice'] - # now get area: use numpy divide where denominator is greater than 0 to avoid divide error - # note, indexing of [:,:,1:] so that annual area array has same shape as flux_div_annual - a_annual = np.divide( - v_annual[:, :, 1:], - h_annual[:, :, 1:], - out=np.full(h_annual[:, :, 1:].shape, np.nan), - where=h_annual[:, :, 1:] > 0, - ) - - # tile to get monthly area, assuming area is constant thoughout the year - a_monthly = np.tile(a_annual, 12) - - # combine monthly thickess and area to get mass - m_monthly = m_spec_monthly * a_monthly - - return h_monthly, m_spec_monthly, m_monthly - - -def update_xrdataset(input_ds, h_monthly, m_spec_monthly, m_monthly): - """ - update xarray dataset to add new fields - - Parameters - ---------- - xrdataset : xarray Dataset - existing xarray dataset - newdata : ndarray - new data array - description: str - describing new data field - - output_ds : xarray Dataset - empty xarray dataset that contains variables and attributes to be filled in by simulation runs - encoding : dictionary - encoding used with exporting xarray dataset to netcdf - """ - # coordinates - glac_values = input_ds.glac.values - time_values = input_ds.time.values - bin_values = input_ds.bin.values - - output_coords_dict = collections.OrderedDict() - output_coords_dict['bin_thick_monthly'] = collections.OrderedDict( - [('glac', glac_values), ('bin', bin_values), ('time', time_values)] - ) - output_coords_dict['bin_mass_spec_monthly'] = collections.OrderedDict( - [('glac', glac_values), ('bin', bin_values), ('time', time_values)] - ) - output_coords_dict['bin_mass_monthly'] = collections.OrderedDict( - [('glac', glac_values), ('bin', bin_values), ('time', time_values)] - ) - - # Attributes dictionary - output_attrs_dict = {} - output_attrs_dict['bin_thick_monthly'] = { - 'long_name': 'binned monthly ice thickness', - 'units': 'm', - 'temporal_resolution': 'monthly', - 'comment': 'monthly ice thickness binned by surface elevation (assuming constant flux divergence throughout a given year)', - } - output_attrs_dict['bin_mass_spec_monthly'] = { - 'long_name': 'binned monthly specific ice mass', - 'units': 'kg m^-2', - 'temporal_resolution': 'monthly', - 'comment': 'monthly ice mass per unit area binned by surface elevation (assuming constant flux divergence throughout a given year)', - } - output_attrs_dict['bin_mass_monthly'] = { - 'long_name': 'binned monthly ice mass', - 'units': 'kg', - 'temporal_resolution': 'monthly', - 'comment': 'monthly ice mass binned by surface elevation (assuming constant flux divergence and area throughout a given year)', - } - - # Add variables to empty dataset and merge together - count_vn = 0 - encoding = {} - for vn in output_coords_dict.keys(): - empty_holder = np.zeros([len(output_coords_dict[vn][i]) for i in list(output_coords_dict[vn].keys())]) - output_ds = xr.Dataset( - {vn: (list(output_coords_dict[vn].keys()), empty_holder)}, - coords=output_coords_dict[vn], - ) - count_vn += 1 - # Merge datasets of stats into one output - if count_vn == 1: - output_ds_all = output_ds - else: - output_ds_all = xr.merge((output_ds_all, output_ds)) - # Add attributes - for vn in output_ds_all.variables: - try: - output_ds_all[vn].attrs = output_attrs_dict[vn] - except: - pass - # Encoding (specify _FillValue, offsets, etc.) - encoding[vn] = {'_FillValue': None, 'zlib': True, 'complevel': 9} - - output_ds_all['bin_thick_monthly'].values = h_monthly - output_ds_all['bin_mass_spec_monthly'].values = m_spec_monthly - output_ds_all['bin_mass_monthly'].values = m_monthly - - return output_ds_all, encoding - - -def run(simpath): - """ - create binned monthly mass change data product - Parameters - ---------- - list_packed_vars : list - list of packed variables that enable the use of parallels - Returns - ------- - binned_ds : netcdf Dataset - updated binned netcdf containing binned monthly ice thickness and mass - """ - - if os.path.isfile(simpath): - # open dataset - binned_ds = xr.open_dataset(simpath) - - # calculate monthly thickness and mass - h_monthly, m_spec_monthly, m_monthly = get_binned_monthly( - binned_ds.bin_massbalclim_monthly.values, - binned_ds.bin_mass_annual.values, - binned_ds.bin_thick_annual.values, - ) - - # update dataset to add monthly mass change - output_ds_binned, encoding_binned = update_xrdataset(binned_ds, h_monthly, m_spec_monthly, m_monthly) - - # close input ds before write - binned_ds.close() - - # append to existing binned netcdf - output_ds_binned.to_netcdf(simpath, mode='a', encoding=encoding_binned, engine='netcdf4') - - # close datasets - output_ds_binned.close() - - return - - -def main(): - time_start = time.time() - args = getparser().parse_args() - - if args.simpath: - # filter out non-file paths - simpath = [p for p in args.simpath if os.path.isfile(p)] - - elif args.binned_simdir: - # get list of sims - simpath = glob.glob(args.binned_simdir + '*.nc') - if simpath: - # number of cores for parallel processing - if args.ncores > 1: - ncores = int(np.min([len(simpath), args.ncores])) - else: - ncores = 1 - - # Parallel processing - print('Processing with ' + str(ncores) + ' cores...') - with multiprocessing.Pool(ncores) as p: - p.map(run, simpath) - - print('Total processing time:', time.time() - time_start, 's') - - -if __name__ == '__main__': - main() diff --git a/pygem/bin/postproc/postproc_binned_subannual_thick.py b/pygem/bin/postproc/postproc_binned_subannual_thick.py new file mode 100644 index 00000000..13479ae3 --- /dev/null +++ b/pygem/bin/postproc/postproc_binned_subannual_thick.py @@ -0,0 +1,356 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2024 Brandon Tober David Rounce + +Distributed under the MIT license + +derive binned subannual ice thickness and mass from PyGEM simulation +""" + +# Built-in libraries +import argparse +import collections +import glob +import json +import multiprocessing +import os +import time +from functools import partial + +# External libraries +import numpy as np +import pandas as pd +import xarray as xr + +# pygem imports +from pygem.setup.config import ConfigManager + +# instantiate ConfigManager +config_manager = ConfigManager() +# read the config +pygem_prms = config_manager.read_config() + + +# ----- FUNCTIONS ----- +def getparser(): + """ + Use argparse to add arguments from the command line + """ + parser = argparse.ArgumentParser(description='process binned subannual ice thickness for PyGEM simulation') + # add arguments + parser.add_argument( + '-simpath', + action='store', + type=str, + nargs='+', + default=None, + help='path to PyGEM binned simulation (can take multiple)', + ) + parser.add_argument( + '-simdir', + action='store', + type=str, + default=None, + help='directory with binned simulations for which to process subannual thickness', + ) + parser.add_argument( + '-ncores', + action='store', + type=int, + default=1, + help='number of simultaneous processes (cores) to use', + ) + parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') + + return parser + + +def get_binned_subannual( + bin_massbalclim, bin_mass_annual, bin_thick_annual, dates_subannual, dates_annual, debug=False +): + """ + funciton to calculate the subannual binned ice thickness and mass + from subannual climatic mass balance and annual mass and ice thickness products. + + to determine subannual thickness and mass, we must account for flux divergence. + this is not so straight-forward, as PyGEM accounts for ice dynamics at the + end of each model year and not on a subannual timestep. + thus, subannual thickness and mass is determined assuming + the flux divergence is constant throughout the year. + + annual flux divergence is first estimated by combining the annual binned change in ice + thickness and the annual binned mass balance. then, assume flux divergence is constant + throughout the year (divide annual by the number of steps in the binned climatic mass + balance to get subannual flux divergence). + + subannual binned flux divergence can then be combined with + subannual binned climatic mass balance to get subannual binned change in ice thickness and mass. + + + Parameters + ---------- + bin_massbalclim : ndarray + climatic mass balance [m w.e. yr^-1] with subannual timesteps (monthly/daily) + shape : [#glac, #elevbins, #steps] + bin_mass_annual : ndarray + annual binned ice mass computed by PyGEM [kg] + shape : [#glac, #elevbins, #years] + bin_thick_annual : ndarray + annual binned glacier thickness [m ice] + shape : [#glac, #elevbins, #years] + dates_subannual : array-like of datetime-like + dates associated with `bin_massbalclim` (subannual) + dates_annual : array-like of datetime-like + dates associated with `bin_thick_annual` and `bin_mass_annual` (annual, values correspond to start of the year) + + + Returns + ------- + h_subannual : ndarray + subannual binned ice thickness [m ice] + shape : [#glac, #elevbins, #steps] + m_spec_subannual : ndarray + subannual binned specific ice mass [kg m^-2] + shape : [#glac, #elevbins, #steps] + m_subannual : ndarray + subannual binned glacier mass [kg] + shape : [#glac, #elevbins, #steps] + """ + + n_glac, n_bins, n_steps = bin_massbalclim.shape + years_annual = np.array([d.year for d in dates_annual]) + years_subannual = np.array([d.year for d in dates_subannual]) + yrs = np.unique(years_subannual) + nyrs = len(yrs) + assert nyrs > 1, 'Need at least two annual steps for flux divergence estimation' + + # --- Step 1: convert mass balance from m w.e. to m ice --- + rho_w = pygem_prms['constants']['density_water'] + rho_i = pygem_prms['constants']['density_ice'] + bin_massbalclim_ice = bin_massbalclim * (rho_w / rho_i) + + # --- Step 2: compute annual cumulative mass balance --- + # Initialize annual cumulative mass balance (exclude last year for flux calculation) + bin_massbalclim_annual = np.zeros((n_glac, n_bins, nyrs)) + for i, year in enumerate(yrs): + idx = np.where(years_subannual == year)[0] + bin_massbalclim_annual[:, :, i] = bin_massbalclim_ice[:, :, idx].sum(axis=-1) + + # --- Step 3: compute annual thickness change --- + bin_delta_thick_annual = np.diff(bin_thick_annual, axis=-1) # [m ice yr^-1] + + # --- Step 4: compute annual flux divergence --- + bin_flux_divergence_annual = bin_massbalclim_annual - bin_delta_thick_annual # [m ice yr^-1] + + # --- Step 5: expand flux divergence to subannual steps --- + bin_flux_divergence_subannual = np.zeros_like(bin_massbalclim_ice) + for i, year in enumerate(yrs): + idx = np.where(years_subannual == year)[0] + bin_flux_divergence_subannual[:, :, idx] = bin_flux_divergence_annual[:, :, i][:, :, np.newaxis] / len(idx) + + # --- Step 6: compute subannual thickness change --- + bin_delta_thick_subannual = bin_massbalclim_ice - bin_flux_divergence_subannual + + # --- Step 7: calculate subannual thickness --- + running_bin_delta_thick_subannual = np.cumsum(bin_delta_thick_subannual, axis=-1) + bin_thick_subannual = running_bin_delta_thick_subannual + bin_thick_annual[:, :, 0][:, :, np.newaxis] + + # --- Step 8: compute glacier volume and area on subannual timestep --- + bin_volume_annual = bin_mass_annual / rho_i # annual volume [m^3] per bin + bin_area_annual = np.divide( + bin_volume_annual[:, :, 1:], # exclude first year to match flux_div_annual + bin_thick_annual[:, :, 1:], + out=np.full(bin_thick_annual[:, :, 1:].shape, np.nan), + where=bin_thick_annual[:, :, 1:] > 0, + ) + + # --- Step 9 : compute subannual glacier mass --- + # First expand area to subannual steps + bin_area_subannual = np.full(bin_massbalclim_ice.shape, np.nan) + for i, year in enumerate(yrs): + idx = np.where(years_subannual == year)[0] + bin_area_subannual[:, :, idx] = bin_area_annual[:, :, i][:, :, np.newaxis] + + # multiply by ice density to get subannual mass + bin_mass_subannual = bin_thick_subannual * rho_i * bin_area_subannual + + # --- Step 10: debug check --- + if debug: + for i, year in enumerate(yrs): + # get last subannual index of that year + idx = np.where(years_subannual == year)[0][-1] + diff = bin_thick_subannual[:, :, idx] - bin_thick_annual[:, :, i + 1] + print(f'Year {year}, subannual idx: {idx}') + print('Max diff:', np.max(np.abs(diff))) + print('Min diff:', np.min(np.abs(diff))) + print('Mean diff:', np.mean(diff)) + print() + # optional assertion + np.testing.assert_allclose( + bin_thick_subannual[:, :, idx], + bin_thick_annual[:, :, i + 1], + rtol=1e-6, + atol=1e-12, + err_msg=f'Mismatch in thickness for year {year}', + ) + + return bin_thick_subannual, bin_mass_subannual + + +def update_xrdataset(input_ds, bin_thick, bin_mass, timestep): + """ + update xarray dataset to add new fields + + Parameters + ---------- + xrdataset : xarray Dataset + existing xarray dataset + newdata : ndarray + new data array + description: str + describing new data field + + output_ds : xarray Dataset + empty xarray dataset that contains variables and attributes to be filled in by simulation runs + encoding : dictionary + encoding used with exporting xarray dataset to netcdf + """ + # coordinates + glac_values = input_ds.glac.values + time_values = input_ds.time.values + bin_values = input_ds.bin.values + + output_coords_dict = collections.OrderedDict() + output_coords_dict['bin_thick'] = collections.OrderedDict( + [('glac', glac_values), ('bin', bin_values), ('time', time_values)] + ) + output_coords_dict['bin_mass'] = collections.OrderedDict( + [('glac', glac_values), ('bin', bin_values), ('time', time_values)] + ) + + # Attributes dictionary + output_attrs_dict = {} + output_attrs_dict['bin_thick'] = { + 'long_name': 'binned ice thickness', + 'units': 'm', + 'temporal_resolution': timestep, + 'comment': 'subannual ice thickness binned by surface elevation (assuming constant flux divergence throughout a given year)', + } + output_attrs_dict['bin_mass'] = { + 'long_name': 'binned ice mass', + 'units': 'kg', + 'temporal_resolution': timestep, + 'comment': 'subannual ice mass binned by surface elevation (assuming constant flux divergence and area throughout a given year)', + } + + # Add variables to empty dataset and merge together + count_vn = 0 + encoding = {} + for vn in output_coords_dict.keys(): + empty_holder = np.zeros([len(output_coords_dict[vn][i]) for i in list(output_coords_dict[vn].keys())]) + output_ds = xr.Dataset( + {vn: (list(output_coords_dict[vn].keys()), empty_holder)}, + coords=output_coords_dict[vn], + ) + count_vn += 1 + # Merge datasets of stats into one output + if count_vn == 1: + output_ds_all = output_ds + else: + output_ds_all = xr.merge((output_ds_all, output_ds)) + # Add attributes + for vn in output_ds_all.variables: + try: + output_ds_all[vn].attrs = output_attrs_dict[vn] + except: + pass + # Encoding (specify _FillValue, offsets, etc.) + encoding[vn] = {'_FillValue': None, 'zlib': True, 'complevel': 9} + + output_ds_all['bin_thick'].values = bin_thick + output_ds_all['bin_mass'].values = bin_mass + + return output_ds_all, encoding + + +def run(simpath, debug): + """ + create binned subannual mass change data product + Parameters + ---------- + list_packed_vars : list + list of packed variables that enable the use of parallels + Returns + ------- + binned_ds : netcdf Dataset + updated binned netcdf containing binned subannual ice thickness and mass + """ + + if os.path.isfile(simpath): + # open dataset + binned_ds = xr.open_dataset(simpath) + + # get model time tables + timestep = json.loads(binned_ds.attrs['model_parameters'])['timestep'] + # get model dates + dates_annual = pd.to_datetime([f'{y}-01-01' for y in binned_ds.year.values]) + dates_subannual = pd.to_datetime(binned_ds.time.values) + + # calculate subannual thickness and mass + bin_thick, bin_mass = get_binned_subannual( + bin_massbalclim=binned_ds.bin_massbalclim.values, + bin_mass_annual=binned_ds.bin_mass_annual.values, + bin_thick_annual=binned_ds.bin_thick_annual.values, + dates_subannual=dates_subannual, + dates_annual=dates_annual, + debug=debug, + ) + + # update dataset to add subannual binned thickness and mass + output_ds_binned, encoding_binned = update_xrdataset( + binned_ds, bin_thick=bin_thick, bin_mass=bin_mass, timestep=timestep + ) + + # close input ds before write + binned_ds.close() + + # append to existing binned netcdf + output_ds_binned.to_netcdf(simpath, mode='a', encoding=encoding_binned, engine='netcdf4') + + # close datasets + output_ds_binned.close() + + return + + +def main(): + time_start = time.time() + args = getparser().parse_args() + + if args.simpath: + # filter out non-file paths + simpath = [p for p in args.simpath if os.path.isfile(p)] + elif args.simdir: + # get list of sims + simpath = sorted(glob.glob(args.simdir + '/*.nc')) + if simpath: + # number of cores for parallel processing + if args.ncores > 1: + ncores = int(np.min([len(simpath), args.ncores])) + else: + ncores = 1 + + # set up partial function with debug argument + run_partial = partial(run, debug=args.debug) + + # Parallel processing + print('Processing with ' + str(ncores) + ' cores...') + with multiprocessing.Pool(ncores) as p: + p.map(run_partial, simpath) + + print('Total processing time:', time.time() - time_start, 's') + + +if __name__ == '__main__': + main() diff --git a/pygem/bin/postproc/postproc_compile_simulations.py b/pygem/bin/postproc/postproc_compile_simulations.py index 391fff75..2247687e 100644 --- a/pygem/bin/postproc/postproc_compile_simulations.py +++ b/pygem/bin/postproc/postproc_compile_simulations.py @@ -11,6 +11,7 @@ # imports import argparse import glob +import json import multiprocessing import os import time @@ -57,67 +58,67 @@ # define metadata for each variable var_metadata = { - 'glac_runoff_monthly': { + 'glac_runoff': { 'long_name': 'glacier-wide runoff', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'runoff from the glacier terminus, which moves over time', }, - 'offglac_runoff_monthly': { + 'offglac_runoff': { 'long_name': 'off-glacier-wide runoff', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'off-glacier runoff from area where glacier no longer exists', }, - 'glac_acc_monthly': { + 'glac_acc': { 'long_name': 'glacier-wide accumulation, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'only the solid precipitation', }, - 'glac_melt_monthly': { + 'glac_melt': { 'long_name': 'glacier-wide melt, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', }, - 'glac_refreeze_monthly': { + 'glac_refreeze': { 'long_name': 'glacier-wide refreeze, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', }, - 'glac_frontalablation_monthly': { + 'glac_frontalablation': { 'long_name': 'glacier-wide frontal ablation, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': ( 'mass losses from calving, subaerial frontal melting, sublimation above the waterline and ' 'subaqueous frontal melting below the waterline; positive values indicate mass lost like melt' ), }, - 'glac_snowline_monthly': { + 'glac_snowline': { 'long_name': 'transient snowline altitude above mean sea level', 'units': 'm', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'transient snowline is altitude separating snow from ice/firn', }, - 'glac_massbaltotal_monthly': { + 'glac_massbaltotal': { 'long_name': 'glacier-wide total mass balance, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'total mass balance is the sum of the climatic mass balance and frontal ablation', }, - 'glac_prec_monthly': { + 'glac_prec': { 'long_name': 'glacier-wide precipitation (liquid)', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': 'only the liquid precipitation, solid precipitation excluded', }, - 'glac_mass_monthly': { + 'glac_mass': { 'long_name': 'glacier mass', 'units': 'kg', - 'temporal_resolution': 'monthly', + 'temporal_resolution': '', 'comment': ( - 'mass of ice based on area and ice thickness at start of the year and the monthly total mass balance' + 'mass of ice based on area and ice thickness at start of the year and the total mass balance over during the model year' ), }, 'glac_area_annual': { @@ -219,13 +220,16 @@ def run(args): + '/' + sim_climate_scenario + '/stats/' - + f'*{gcm}_{sim_climate_scenario}_{realizations[0]}_{calibration}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc'.replace( + + f'*{gcm}_{sim_climate_scenario}_{realizations[0]}_{calibration}_ba{bias_adj}_*sets_{sim_startyear}_{sim_endyear}_all.nc'.replace( '__', '_' ) )[0] else: fp = glob.glob( - base_dir + gcm + '/stats/' + f'*{gcm}_{calibration}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc' + base_dir + + gcm + + '/stats/' + + f'*{gcm}_{calibration}_ba{bias_adj}_*sets_{sim_startyear}_{sim_endyear}_all.nc' )[0] # get number of sets from file name nsets = fp.split('/')[-1].split('_')[-4] @@ -233,6 +237,12 @@ def run(args): ds_glac = xr.open_dataset(fp) year_values = ds_glac.year.values time_values = ds_glac.time.values + # get model time step + timestep = json.loads(ds_glac.attrs['model_parameters'])['timestep'] + if timestep.lower() in ['daily']: + period_name = 'day' + elif timestep.lower() in ['monthly']: + period_name = 'month' # check if desired vars are in ds ds_vars = list(ds_glac.keys()) missing_vars = list(set(vars) - set(ds_vars)) @@ -317,19 +327,19 @@ def run(args): try: arr = getattr(ds_glac, var).values except AttributeError: - if 'monthly' in var: - arr = np.full((1, len(time_values)), np.nan) - elif 'annual' in var: + if 'annual' in var: arr = np.full((1, year_values.shape[0]), np.nan) + else: + arr = np.full((1, len(time_values)), np.nan) reg_gcm_data[var].append(arr) # if glacier output DNE in sim output file, create empty nan arrays to keep record of missing glaciers except: for var in vars: - if 'monthly' in var: - arr = np.full((1, len(time_values)), np.nan) - elif 'annual' in var: + if 'annual' in var: arr = np.full((1, year_values.shape[0]), np.nan) + else: + arr = np.full((1, len(time_values)), np.nan) reg_gcm_data[var].append(arr) # stack all individual glacier data for each var, for the current GCM and append as 2D array to list of reg_all_gcms_data[var] @@ -408,6 +418,8 @@ def run(args): ]: if attr_key in meta: ds[var].attrs[attr_key] = meta[attr_key] + if attr_key == 'temporal_resolution' and 'annual' not in var: + ds[var].attrs[attr_key] = timestep ds[var].attrs['grid_mapping'] = 'crs' # crs attributes - same for all vars @@ -425,10 +437,9 @@ def run(args): if 'annual' in var: ds.time.attrs['range'] = str(year_values[0]) + ' - ' + str(year_values[-1]) ds.time.attrs['comment'] = 'years referring to the start of each year' - elif 'monthly' in var: + else: ds.time.attrs['range'] = str(time_values[0]) + ' - ' + str(time_values[-1]) - ds.time.attrs['comment'] = 'start of the month' - + ds.time.attrs['comment'] = f'start of the {period_name}' ds.RGIId.attrs['long_name'] = 'Randolph Glacier Inventory Id' ds.RGIId.attrs['comment'] = 'RGIv6.0 (https://nsidc.org/data/nsidc-0770/versions/6)' ds.RGIId.attrs['cf_role'] = 'timeseries_id' @@ -590,18 +601,18 @@ def main(): parser.add_argument( '-vars', type=str, - help='comm delimited list of PyGEM variables to compile (can take multiple, ex. "monthly_mass annual_area")', + help='comm delimited list of PyGEM variables to compile (can take multiple, ex. "glac_mass glac_area_annual")', choices=[ - 'glac_runoff_monthly', - 'offglac_runoff_monthly', - 'glac_acc_monthly', - 'glac_melt_monthly', - 'glac_refreeze_monthly', - 'glac_frontalablation_monthly', - 'glac_snowline_monthly', - 'glac_massbaltotal_monthly', - 'glac_prec_monthly', - 'glac_mass_monthly', + 'glac_runoff', + 'offglac_runoff', + 'glac_acc', + 'glac_melt', + 'glac_refreeze', + 'glac_frontalablation', + 'glac_snowline', + 'glac_massbaltotal', + 'glac_prec', + 'glac_mass', 'glac_mass_annual', 'glac_area_annual', 'glac_ELA_annual', @@ -669,16 +680,16 @@ def main(): if not vars: vars = [ - 'glac_runoff_monthly', - 'offglac_runoff_monthly', - 'glac_acc_monthly', - 'glac_melt_monthly', - 'glac_refreeze_monthly', - 'glac_frontalablation_monthly', - 'glac_snowline_monthly', - 'glac_massbaltotal_monthly', - 'glac_prec_monthly', - 'glac_mass_monthly', + 'glac_runoff', + 'offglac_runoff', + 'glac_acc', + 'glac_melt', + 'glac_refreeze', + 'glac_frontalablation', + 'glac_snowline', + 'glac_massbaltotal', + 'glac_prec', + 'glac_mass', 'glac_mass_annual', 'glac_area_annual', 'glac_ELA_annual', diff --git a/pygem/bin/postproc/postproc_monthly_mass.py b/pygem/bin/postproc/postproc_subannual_mass.py similarity index 52% rename from pygem/bin/postproc/postproc_monthly_mass.py rename to pygem/bin/postproc/postproc_subannual_mass.py index 819c2591..7931d937 100644 --- a/pygem/bin/postproc/postproc_monthly_mass.py +++ b/pygem/bin/postproc/postproc_subannual_mass.py @@ -5,18 +5,22 @@ Distributed under the MIT license -derive monthly glacierwide mass for PyGEM simulation using annual glacier mass and monthly total mass balance +derive sub-annual glacierwide mass for PyGEM simulation using annual glacier mass and sub-annual total mass balance """ # Built-in libraries import argparse import collections import glob +import json import multiprocessing import os import time +from functools import partial +import matplotlib.pyplot as plt import numpy as np +import pandas as pd # External libraries import xarray as xr @@ -36,7 +40,7 @@ def getparser(): Use argparse to add arguments from the command line """ parser = argparse.ArgumentParser( - description='process monthly glacierwide mass from annual mass and total monthly mass balance' + description='process sub-annual glacierwide mass from annual mass and total sub-annual mass balance' ) # add arguments parser.add_argument( @@ -51,7 +55,7 @@ def getparser(): action='store', type=str, default=None, - help='directory with glacierwide simulation outputs for which to process monthly mass', + help='directory with glacierwide simulation outputs for which to process sub-annual mass', ) parser.add_argument( '-ncores', @@ -60,14 +64,14 @@ def getparser(): default=1, help='number of simultaneous processes (cores) to use', ) - + parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') return parser -def get_monthly_mass(glac_mass_annual, glac_massbaltotal_monthly): +def get_subannual_mass(df_annual, df_sub, debug=False): """ - funciton to calculate the monthly glacier mass - from annual glacier mass and monthly total mass balance + funciton to calculate the sub-annual glacier mass + from annual glacier mass and sub-annual total mass balance Parameters ---------- @@ -75,35 +79,65 @@ def get_monthly_mass(glac_mass_annual, glac_massbaltotal_monthly): ndarray containing the annual glacier mass for each year computed by PyGEM shape: [#glac, #years] unit: kg - glac_massbaltotal_monthly : float - ndarray containing the monthly total mass balance computed by PyGEM - shape: [#glac, #months] + glac_massbaltotal : float + ndarray containing the total mass balance computed by PyGEM + shape: [#glac, #steps] unit: kg Returns ------- - glac_mass_monthly: float - ndarray containing the monthly glacier mass - shape : [#glac, #months] + glac_mass: float + ndarray containing the running glacier mass + shape : [#glac, #steps] unit: kg """ - # get running total monthly mass balance - reshape into subarrays of all values for a given year, then take cumulative sum - oshape = glac_massbaltotal_monthly.shape - running_glac_massbaltotal_monthly = ( - np.reshape(glac_massbaltotal_monthly, (-1, 12), order='C').cumsum(axis=-1).reshape(oshape) - ) - # tile annual mass to then superimpose atop running glacier mass balance (trim off final year from annual mass) - glac_mass_monthly = np.repeat(glac_mass_annual[:, :-1], 12, axis=-1) + # ensure datetime and sorted + df_annual['time'] = pd.to_datetime(df_annual['time']) + df_sub['time'] = pd.to_datetime(df_sub['time']) + df_annual = df_annual.sort_values('time').reset_index(drop=True) + df_sub = df_sub.sort_values('time').reset_index(drop=True) + + # year columns + df_annual['year'] = df_annual['time'].dt.year + df_sub['year'] = df_sub['time'].dt.year + + # map annual starting mass to sub rows + annual_by_year = df_annual.set_index('year')['mass'] + df_sub['annual_mass'] = df_sub['year'].map(annual_by_year) + + # shift massbaltotal within each year so the Jan value doesn't affect Jan mass itself + # i.e., massbaltotal at Jan-01 contributes to Feb-01 mass + df_sub['mb_shifted'] = df_sub.groupby('year')['massbaltotal'].shift(1).fillna(0.0) + + # cumulative sum of shifted values within each year + df_sub['cum_mb_since_year_start'] = df_sub.groupby('year')['mb_shifted'].cumsum() + + # compute sub-annual mass + df_sub['mass'] = df_sub['annual_mass'] + df_sub['cum_mb_since_year_start'] - # add annual mass values to running glacier mass balance - glac_mass_monthly += running_glac_massbaltotal_monthly + if debug: + # --- Quick plot of Jan start points (sub vs annual) --- + # Plot all sub-annual masses as a line + plt.figure(figsize=(12, 5)) + plt.plot(df_sub['time'], df_sub['mass'], label='Sub-annual mass', color='blue') - return glac_mass_monthly + # Overlay annual masses as points/line + plt.plot(df_annual['time'], df_annual['mass'], 'o--', label='Annual mass', color='orange', markersize=6) + # Labels and legend + plt.xlabel('Time') + plt.ylabel('Glacier Mass') + plt.title('Sub-annual Glacier Mass vs Annual Mass') + plt.legend() + plt.tight_layout() + plt.show() -def update_xrdataset(input_ds, glac_mass_monthly): + return df_sub['mass'].values + + +def update_xrdataset(input_ds, glac_mass, timestep): """ update xarray dataset to add new fields @@ -126,15 +160,15 @@ def update_xrdataset(input_ds, glac_mass_monthly): time_values = input_ds.time.values output_coords_dict = collections.OrderedDict() - output_coords_dict['glac_mass_monthly'] = collections.OrderedDict([('glac', glac_values), ('time', time_values)]) + output_coords_dict['glac_mass'] = collections.OrderedDict([('glac', glac_values), ('time', time_values)]) # Attributes dictionary output_attrs_dict = {} - output_attrs_dict['glac_mass_monthly'] = { + output_attrs_dict['glac_mass'] = { 'long_name': 'glacier mass', 'units': 'kg', - 'temporal_resolution': 'monthly', - 'comment': 'monthly glacier mass', + 'temporal_resolution': timestep, + 'comment': 'glacier mass', } # Add variables to empty dataset and merge together @@ -160,15 +194,14 @@ def update_xrdataset(input_ds, glac_mass_monthly): pass # Encoding (specify _FillValue, offsets, etc.) encoding[vn] = {'_FillValue': None, 'zlib': True, 'complevel': 9} - - output_ds_all['glac_mass_monthly'].values = glac_mass_monthly + output_ds_all['glac_mass'].values = glac_mass[np.newaxis, :] return output_ds_all, encoding -def run(simpath): +def run(simpath, debug=False): """ - create monthly mass data product + create sub-annual mass data product Parameters ---------- simpath : str @@ -178,16 +211,27 @@ def run(simpath): try: # open dataset statsds = xr.open_dataset(simpath) - - # calculate monthly mass - pygem glac_massbaltotal_monthly is in units of m3, so convert to mass using density of ice - glac_mass_monthly = get_monthly_mass( - statsds.glac_mass_annual.values, - statsds.glac_massbaltotal_monthly.values * pygem_prms['constants']['density_ice'], + timestep = json.loads(statsds.attrs['model_parameters'])['timestep'] + yvals = statsds.year.values + # convert to pandas dataframe with annual mass + annual_df = pd.DataFrame( + {'time': pd.to_datetime([f'{y}-01-01' for y in yvals]), 'mass': statsds.glac_mass_annual[0].values} ) + tvals = statsds.time.values + # convert to pandas dataframe with sub-annual mass balance + steps_df = pd.DataFrame( + { + 'time': pd.to_datetime([t.strftime('%Y-%m-%d') for t in tvals]), + 'massbaltotal': statsds.glac_massbaltotal[0].values * pygem_prms['constants']['density_ice'], + } + ) + + # calculate sub-annual mass - pygem glac_massbaltotal is in units of m3, so convert to mass using density of ice + glac_mass = get_subannual_mass(annual_df, steps_df, debug=debug) statsds.close() - # update dataset to add monthly mass change - output_ds_stats, encoding = update_xrdataset(statsds, glac_mass_monthly) + # update dataset to add sub-annual mass change + output_ds_stats, encoding = update_xrdataset(statsds, glac_mass, timestep) # close input ds before write statsds.close() @@ -225,10 +269,13 @@ def main(): else: ncores = 1 + # set up partial function with debug argument + run_partial = partial(run, debug=args.debug) + # Parallel processing print('Processing with ' + str(ncores) + ' cores...') with multiprocessing.Pool(ncores) as p: - p.map(run, simpath) + p.map(run_partial, simpath) print('Total processing time:', time.time() - time_start, 's') diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index 60c088da..a25597e9 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -318,7 +318,7 @@ def calculate_elev_change_1d( ev_model.mb_model.glac_wide_massbaltotal + ev_model.mb_model.glac_wide_frontalablation ) - mod_glacierwide_mb_mwea = ( + glacierwide_mb_mwea = ( mbmod.glac_wide_massbaltotal[gdir.mbdata['t1_idx'] : gdir.mbdata['t2_idx'] + 1].sum() / mbmod.glac_wide_area_annual[0] / gdir.mbdata['nyears'] @@ -329,62 +329,63 @@ def calculate_elev_change_1d( except RuntimeError: return float('-inf'), float('-inf') - ### get monthly ice thickness - # grab components of interest - thickness_m = ds[0].thickness_m.values.T # glacier thickness [m ice], (nbins, nyears) - - # set any < 0 thickness to nan - thickness_m[thickness_m <= 0] = np.nan - - # climatic mass balance - dotb_monthly = mbmod.glac_bin_massbalclim # climatic mass balance [m w.e.] per month - # convert to m ice - dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) - - ### to get monthly thickness and mass we require monthly flux divergence ### - # we'll assume the flux divergence is constant througohut the year - # ie. take annual values and divide by 12 - use numpy repeat to repeat values across 12 months - flux_div_monthly_mmo = np.repeat(-ds[0].flux_divergence_myr.values.T[:, 1:] / 12, 12, axis=-1) - - # get monthly binned change in thickness - delta_h_monthly = dotb_monthly - flux_div_monthly_mmo # [m ice per month] - - # get binned monthly thickness = running thickness change + initial thickness - running_delta_h_monthly = np.cumsum(delta_h_monthly, axis=-1) - h_monthly = running_delta_h_monthly + thickness_m[:, 0][:, np.newaxis] - + ### get subannual elevation change + + # --- Step 1: convert mass balance from m w.e. to m ice --- + rho_w = pygem_prms['constants']['density_water'] + rho_i = pygem_prms['constants']['density_ice'] + bin_massbalclim_ice = mbmod.glac_bin_massbalclim * (rho_w / rho_i) # binned climatic mass balance (nbins, nsteps) + + # --- Step 2: expand flux divergence to subannual steps --- + # assume the flux divergence is constant througohut the year + # ie. take annual values and divide spread uniformly throughout model year + bin_flux_divergence_subannual = np.zeros_like(bin_massbalclim_ice) + for i, year in enumerate(gdir.dates_table.year.unique()): + idx = np.where(gdir.dates_table.year.values == year)[0] + bin_flux_divergence_subannual[:, idx] = -ds[0].flux_divergence_myr.values.T[:, i + 1][:, np.newaxis] / len( + idx + ) # note, oggm flux_divergence_myr is opposite sign of convention, hence negative + + # --- Step 3: compute subannual thickness change --- + bin_delta_thick_subannual = bin_massbalclim_ice - bin_flux_divergence_subannual # [m ice] + + # --- Step 4: calculate subannual thickness --- + # calculate binned subannual thickness = running thickness change + initial thickness + bin_thick_initial = ds[0].thickness_m.isel(time=0).values # initial glacier thickness [m ice], (nbins) + running_bin_delta_thick_subannual = np.cumsum(bin_delta_thick_subannual, axis=-1) + bin_thick_subannual = running_bin_delta_thick_subannual + bin_thick_initial[:, np.newaxis] + + # --- Step 5: rebin subannual thickness --- # get surface height at the specified reference year - ref_surface_h = ds[0].bed_h.values + ds[0].thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values - + ref_surface_height = ds[0].bed_h.values + ds[0].thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values # aggregate model bin thicknesses as desired with warnings.catch_warnings(): warnings.filterwarnings('ignore') - h_monthly = np.column_stack( + bin_thick_subannual = np.column_stack( [ stats.binned_statistic( - x=ref_surface_h, + x=ref_surface_height, values=x, statistic=np.nanmean, bins=gdir.elev_change_1d['bin_edges'], )[0] - for x in h_monthly.T + for x in bin_thick_subannual.T ] ) - # interpolate over any empty bins - h_monthly_ = np.column_stack([interp1d_fill_gaps(x.copy()) for x in h_monthly.T]) + bin_thick_subannual = np.column_stack([interp1d_fill_gaps(x.copy()) for x in bin_thick_subannual.T]) - # difference each set of inds in diff_inds_map - mod_elev_change_1d = np.column_stack( + # --- Step 6: calculate elevation change --- + elev_change_1d = np.column_stack( [ - h_monthly_[:, tup[1]] - h_monthly_[:, tup[0]] + bin_thick_subannual[:, tup[1]] - bin_thick_subannual[:, tup[0]] if tup[0] is not None and tup[1] is not None - else np.full(h_monthly_.shape[0], np.nan) + else np.full(bin_thick_subannual.shape[0], np.nan) for tup in gdir.elev_change_1d['model2obs_inds_map'] ] ) - return mod_glacierwide_mb_mwea, mod_elev_change_1d + return glacierwide_mb_mwea, elev_change_1d # class for Gaussian Process model for mass balance emulator @@ -726,6 +727,7 @@ def run(list_packed_vars): # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== try: + # Note this is where pre-processing of datasets (e.g., mass balance, debris) occurs if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: gdir = oggm_compat.single_flowline_glacier_directory(glacier_str) gdir.is_tidewater = False @@ -1824,7 +1826,7 @@ def calc_mb_total_minelev(modelprms): ] # Melt # energy available for melt [degC day] - melt_energy_available = T_minelev * dates_table['daysinmonth'].values + melt_energy_available = T_minelev * dates_table['days_in_step'].values melt_energy_available[melt_energy_available < 0] = 0 # assume all snow melt because anything more would melt underlying ice in lowermost bin # SNOW MELT [m w.e.] diff --git a/pygem/bin/run/run_simulation.py b/pygem/bin/run/run_simulation.py index aa252914..3a3bc0fd 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -17,7 +17,6 @@ # Built-in libraries import argparse import copy -import inspect import json import multiprocessing import os @@ -406,6 +405,10 @@ def run(list_packed_vars): print('sim years:', args.sim_startyear, args.sim_endyear) # ===== LOAD CLIMATE DATA ===== + # Future simulations are not yet set up with daily data + if pygem_prms['time']['timestep'] == 'daily': + assert args.sim_endyear <= 2025, 'Future daily data is not yet available' + # Climate class if sim_climate_name in ['ERA5', 'COAWST']: gcm = class_climate.GCM(name=sim_climate_name) @@ -482,6 +485,7 @@ def run(list_packed_vars): args.sim_startyear, args.ref_startyear, ) + # OPTION 2: Adjust temp and prec using Huss and Hock (2015) elif args.option_bias_adjustment == 2: # Temperature bias correction @@ -536,11 +540,13 @@ def run(list_packed_vars): gcm_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table.shape[0])) ref_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table_ref.shape[0])) elif pygem_prms['mb']['option_ablation'] == 2 and sim_climate_name in ['ERA5']: + assert pygem_prms['time']['timestep'] != 'daily', 'Option 2 for ablation should not be used with daily data' gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( gcm.tempstd_fn, gcm.tempstd_vn, main_glac_rgi, dates_table, verbose=debug ) ref_tempstd = gcm_tempstd elif pygem_prms['mb']['option_ablation'] == 2 and args.ref_climate_name in ['ERA5']: + assert pygem_prms['time']['timestep'] != 'daily', 'Option 2 for ablation should not be used with daily data' # Compute temp std based on reference climate data ref_tempstd, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( ref_gcm.tempstd_fn, @@ -556,29 +562,28 @@ def run(list_packed_vars): ref_tempstd = np.zeros((main_glac_rgi.shape[0], dates_table_ref.shape[0])) # Lapse rate - if sim_climate_name == 'ERA5': - gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, - gcm.lr_vn, - main_glac_rgi, - dates_table, - upscale_var_timestep=True, - verbose=debug, - ) - ref_lr = gcm_lr + if pygem_prms['sim']['params']['use_constant_lapserate']: + gcm_lr = np.zeros((main_glac_rgi.shape[0], dates_table.shape[0])) + pygem_prms['sim']['params']['lapserate'] + ref_lr = np.zeros((main_glac_rgi.shape[0], dates_table_ref.shape[0])) + pygem_prms['sim']['params']['lapserate'] else: - # Compute lapse rates based on reference climate data - ref_lr, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( - ref_gcm.lr_fn, ref_gcm.lr_vn, main_glac_rgi, dates_table_ref, verbose=debug - ) - # Monthly average from reference climate data - gcm_lr = gcmbiasadj.monthly_avg_array_rolled( - ref_lr, - dates_table_ref, - dates_table_full, - args.sim_startyear, - args.ref_startyear, - ) + if sim_climate_name in ['ERA-Interim', 'ERA5']: + gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( + gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, upscale_var_timestep=True, verbose=debug + ) + ref_lr = gcm_lr + else: + # Compute lapse rates based on reference climate data + ref_lr, ref_dates = ref_gcm.importGCMvarnearestneighbor_xarray( + ref_gcm.lr_fn, ref_gcm.lr_vn, main_glac_rgi, dates_table_ref + ) + # Monthly average from reference climate data + gcm_lr = gcmbiasadj.monthly_avg_array_rolled( + ref_lr, + dates_table_ref, + dates_table_full, + args.sim_startyear, + args.ref_startyear, + ) # ===== RUN MASS BALANCE ===== # Number of simulations @@ -587,9 +592,9 @@ def run(list_packed_vars): else: nsims = 1 - # Number of years - nyears = dates_table.year.unique()[-1] - dates_table.year.unique()[0] + 1 - nyears_ref = dates_table_ref.year.unique()[-1] - dates_table.year.unique()[0] + 1 + # Number of years (for OGGM's run_until_and_store) + nyears = len(dates_table.year.unique()) + nyears_ref = len(dates_table_ref.year.unique()) for glac in range(main_glac_rgi.shape[0]): if glac == 0: @@ -605,8 +610,6 @@ def run(list_packed_vars): rgiid = main_glac_rgi.loc[main_glac_rgi.index.values[glac], 'RGIId'] try: - # for batman in [0]: - # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== if glacier_rgi_table['TermType'] not in [1, 5] or not pygem_prms['setup']['include_frontalablation']: gdir = single_flowline_glacier_directory(glacier_str, working_dir=args.oggm_working_dir) @@ -813,31 +816,32 @@ def run(list_packed_vars): # Time attributes and values if pygem_prms['climate']['sim_wateryear'] == 'hydro': - annual_columns = np.unique(dates_table['wateryear'].values)[0 : int(dates_table.shape[0] / 12)] + annual_columns = np.unique(dates_table['wateryear'].values) else: - annual_columns = np.unique(dates_table['year'].values)[0 : int(dates_table.shape[0] / 12)] + annual_columns = np.unique(dates_table['year'].values) + # append additional year to year_values to account for mass and area at end of period year_values = annual_columns year_values = np.concatenate((year_values, np.array([annual_columns[-1] + 1]))) - output_glac_temp_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_prec_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_acc_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_refreeze_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_melt_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_frontalablation_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_massbaltotal_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_runoff_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_glac_snowline_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_temp_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_prec_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_acc_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_refreeze_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_melt_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_frontalablation_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_massbaltotal_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_runoff_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_glac_snowline_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan output_glac_area_annual = np.zeros((year_values.shape[0], nsims)) * np.nan output_glac_mass_annual = np.zeros((year_values.shape[0], nsims)) * np.nan output_glac_mass_bsl_annual = np.zeros((year_values.shape[0], nsims)) * np.nan output_glac_mass_change_ignored_annual = np.zeros((year_values.shape[0], nsims)) output_glac_ELA_annual = np.zeros((year_values.shape[0], nsims)) * np.nan - output_offglac_prec_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_offglac_refreeze_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_offglac_melt_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_offglac_snowpack_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan - output_offglac_runoff_monthly = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_prec_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_refreeze_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_melt_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_snowpack_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan + output_offglac_runoff_steps = np.zeros((dates_table.shape[0], nsims)) * np.nan output_glac_bin_icethickness_annual = None # Loop through model parameters @@ -875,7 +879,6 @@ def run(list_packed_vars): + str(np.round(modelprms['tbias'], 2)) ) - # %% # ----- ICE THICKNESS INVERSION using OGGM ----- if args.option_dynamics is not None: # Apply inversion_filter on mass balance with debris to avoid negative flux @@ -960,7 +963,9 @@ def run(list_packed_vars): option_areaconstant=False, ) - # Glacier dynamics model + ###################################### + ### OGGM dynamical evolution model ### + ###################################### if args.option_dynamics == 'OGGM': if debug: print('OGGM GLACIER DYNAMICS!') @@ -988,188 +993,64 @@ def run(list_packed_vars): if debug: fig, ax = plt.subplots(1) - graphics.plot_modeloutput_section(ev_model, ax=ax) - - try: - diag = ev_model.run_until_and_store(args.sim_endyear + 1) - ev_model.mb_model.glac_wide_volume_annual[-1] = diag.volume_m3[-1] - ev_model.mb_model.glac_wide_area_annual[-1] = diag.area_m2[-1] - - # Record frontal ablation for tidewater glaciers and update total mass balance - if gdir.is_tidewater: - # Glacier-wide frontal ablation (m3 w.e.) - # - note: diag.calving_m3 is cumulative calving - if debug: - print('\n\ndiag.calving_m3:', diag.calving_m3.values) - print( - 'calving_m3_since_y0:', - ev_model.calving_m3_since_y0, - ) - calving_m3_annual = ( - (diag.calving_m3.values[1:] - diag.calving_m3.values[0:-1]) - * pygem_prms['constants']['density_ice'] - / pygem_prms['constants']['density_water'] - ) - for n in np.arange(calving_m3_annual.shape[0]): - ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3_annual[n] + graphics.plot_modeloutput_section( + ev_model, ax=ax, lnlabel=f'Glacier year {args.sim_startyear}' + ) - # Glacier-wide total mass balance (m3 w.e.) - ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation - ) + diag = ev_model.run_until_and_store(args.sim_endyear + 1) + ev_model.mb_model.glac_wide_volume_annual[-1] = diag.volume_m3[-1] + ev_model.mb_model.glac_wide_area_annual[-1] = diag.area_m2[-1] - if debug: - print( - 'avg calving_m3:', - calving_m3_annual.sum() / nyears, - ) - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, - 4, - ), - ) - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.calving_m3_since_y0 - * pygem_prms['constants']['density_ice'] - / 1e12 - / nyears, - 4, - ), - ) - - except RuntimeError as e: - if 'Glacier exceeds domain boundaries' in repr(e): - count_exceed_boundary_errors += 1 - successful_run = False - - # LOG FAILURE - fail_domain_fp = ( - pygem_prms['root'] - + '/Output/simulations/fail-exceed_domain/' - + reg_str - + '/' - + sim_climate_name - + '/' - ) - if sim_climate_name not in [ - 'ERA5', - 'COAWST', - ]: - fail_domain_fp += sim_climate_scenario + '/' - if not os.path.exists(fail_domain_fp): - os.makedirs(fail_domain_fp, exist_ok=True) - txt_fn_fail = glacier_str + '-sim_failed.txt' - with open(fail_domain_fp + txt_fn_fail, 'w') as text_file: - text_file.write( - glacier_str - + ' failed to complete ' - + str(count_exceed_boundary_errors) - + ' simulations' - ) - elif gdir.is_tidewater: - if debug: - print('OGGM dynamics failed, using mass redistribution curves') - # Mass redistribution curves glacier dynamics model - ev_model = MassRedistributionCurveModel( - nfls, - mb_model=mbmod, - y0=args.sim_startyear, - glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, - fs=fs, - is_tidewater=gdir.is_tidewater, - water_level=water_level, - ) - _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) - ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values - ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values - - # Record frontal ablation for tidewater glaciers and update total mass balance - # Update glacier-wide frontal ablation (m3 w.e.) - ev_model.mb_model.glac_wide_frontalablation = ( - ev_model.mb_model.glac_bin_frontalablation.sum(0) - ) - # Update glacier-wide total mass balance (m3 w.e.) - ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation + # Record frontal ablation for tidewater glaciers and update total mass balance + if gdir.is_tidewater: + # Glacier-wide frontal ablation (m3 w.e.) + # - note: diag.calving_m3 is cumulative calving + if debug: + print('\n\ndiag.calving_m3:', diag.calving_m3.values) + print( + 'calving_m3_since_y0:', + ev_model.calving_m3_since_y0, ) + calving_m3_annual = ( + (diag.calving_m3.values[1:] - diag.calving_m3.values[0:-1]) + * pygem_prms['constants']['density_ice'] + / pygem_prms['constants']['density_water'] + ) + for n, year in enumerate(np.arange(args.sim_startyear, args.sim_endyear + 1)): + tstart, tstop = ev_model.mb_model.get_step_inds(year) + ev_model.mb_model.glac_wide_frontalablation[tstop] = calving_m3_annual[n] - if debug: - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, - 4, - ), - ) - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.calving_m3_since_y0 - * pygem_prms['constants']['density_ice'] - / 1e12 - / nyears, - 4, - ), - ) + # Glacier-wide total mass balance (m3 w.e.) + ev_model.mb_model.glac_wide_massbaltotal = ( + ev_model.mb_model.glac_wide_massbaltotal - ev_model.mb_model.glac_wide_frontalablation + ) - except: - if gdir.is_tidewater: - if debug: - print('OGGM dynamics failed, using mass redistribution curves') - # Mass redistribution curves glacier dynamics model - ev_model = MassRedistributionCurveModel( - nfls, - mb_model=mbmod, - y0=args.sim_startyear, - glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, - fs=fs, - is_tidewater=gdir.is_tidewater, - water_level=water_level, + if debug: + print( + 'avg calving_m3:', + calving_m3_annual.sum() / nyears, ) - _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) - ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values - ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values - - # Record frontal ablation for tidewater glaciers and update total mass balance - # Update glacier-wide frontal ablation (m3 w.e.) - ev_model.mb_model.glac_wide_frontalablation = ( - ev_model.mb_model.glac_bin_frontalablation.sum(0) + print( + 'avg frontal ablation [Gta]:', + np.round( + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, + 4, + ), ) - # Update glacier-wide total mass balance (m3 w.e.) - ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation + print( + 'avg frontal ablation [Gta]:', + np.round( + ev_model.calving_m3_since_y0 + * pygem_prms['constants']['density_ice'] + / 1e12 + / nyears, + 4, + ), ) - if debug: - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, - 4, - ), - ) - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.calving_m3_since_y0 - * pygem_prms['constants']['density_ice'] - / 1e12 - / nyears, - 4, - ), - ) - - else: - raise - - # Mass redistribution model + ###################################### + ##### mass redistribution model ##### + ###################################### elif args.option_dynamics == 'MassRedistributionCurves': if debug: print('MASS REDISTRIBUTION CURVES!') @@ -1180,82 +1061,54 @@ def run(list_packed_vars): glen_a=cfg.PARAMS['glen_a'] * glen_a_multiplier, fs=fs, is_tidewater=gdir.is_tidewater, - # water_level=gdir.get_diagnostics().get('calving_water_level', None) + # water_level=gdir.get_diagnostics().get('calving_water_level', None) water_level=water_level, ) if debug: fig, ax = plt.subplots(1) - graphics.plot_modeloutput_section(ev_model, ax=ax) - try: - _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) - # print('shape of volume:', ev_model.mb_model.glac_wide_volume_annual.shape, diag.volume_m3.shape) - ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values - ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values - - # Record frontal ablation for tidewater glaciers and update total mass balance - if gdir.is_tidewater: - # Update glacier-wide frontal ablation (m3 w.e.) - ev_model.mb_model.glac_wide_frontalablation = ( - ev_model.mb_model.glac_bin_frontalablation.sum(0) - ) - # Update glacier-wide total mass balance (m3 w.e.) - ev_model.mb_model.glac_wide_massbaltotal = ( - ev_model.mb_model.glac_wide_massbaltotal - - ev_model.mb_model.glac_wide_frontalablation - ) + graphics.plot_modeloutput_section( + ev_model, ax=ax, lnlabel=f'Glacier year {args.sim_startyear}' + ) - if debug: - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, - 4, - ), - ) - print( - 'avg frontal ablation [Gta]:', - np.round( - ev_model.calving_m3_since_y0 - * pygem_prms['constants']['density_ice'] - / 1e12 - / nyears, - 4, - ), - ) + _, diag = ev_model.run_until_and_store(args.sim_endyear + 1) + # print('shape of volume:', ev_model.mb_model.glac_wide_volume_annual.shape, diag.volume_m3.shape) + ev_model.mb_model.glac_wide_volume_annual = diag.volume_m3.values + ev_model.mb_model.glac_wide_area_annual = diag.area_m2.values + + # Record frontal ablation for tidewater glaciers and update total mass balance + if gdir.is_tidewater: + # Update glacier-wide frontal ablation (m3 w.e.) + ev_model.mb_model.glac_wide_frontalablation = ( + ev_model.mb_model.glac_bin_frontalablation.sum(0) + ) + # Update glacier-wide total mass balance (m3 w.e.) + ev_model.mb_model.glac_wide_massbaltotal = ( + ev_model.mb_model.glac_wide_massbaltotal - ev_model.mb_model.glac_wide_frontalablation + ) - except RuntimeError as e: - if 'Glacier exceeds domain boundaries' in repr(e): - count_exceed_boundary_errors += 1 - successful_run = False - - # LOG FAILURE - fail_domain_fp = ( - pygem_prms['root'] - + '/Output/simulations/fail-exceed_domain/' - + reg_str - + '/' - + sim_climate_name - + '/' + if debug: + print( + 'avg frontal ablation [Gta]:', + np.round( + ev_model.mb_model.glac_wide_frontalablation.sum() / 1e9 / nyears, + 4, + ), + ) + print( + 'avg frontal ablation [Gta]:', + np.round( + ev_model.calving_m3_since_y0 + * pygem_prms['constants']['density_ice'] + / 1e12 + / nyears, + 4, + ), ) - if sim_climate_name not in [ - 'ERA5', - 'COAWST', - ]: - fail_domain_fp += sim_climate_scenario + '/' - if not os.path.exists(fail_domain_fp): - os.makedirs(fail_domain_fp, exist_ok=True) - txt_fn_fail = glacier_str + '-sim_failed.txt' - with open(fail_domain_fp + txt_fn_fail, 'w') as text_file: - text_file.write( - glacier_str - + ' failed to complete ' - + str(count_exceed_boundary_errors) - + ' simulations' - ) - else: - raise + ###################################### + ######### no dynamical model ######### + ###################################### elif args.option_dynamics is None: # Mass balance model ev_model = None @@ -1271,25 +1124,20 @@ def run(list_packed_vars): years = np.arange(args.sim_startyear, args.sim_endyear + 1) mb_all = [] for year in years: - mb_annual = mbmod.get_annual_mb( + # Calculate annual mass balance + mbmod.get_annual_mb( nfls[0].surface_h, fls=nfls, fl_id=0, year=year, - debug=True, + debug=debug, ) - mb_mwea = ( - mb_annual - * 365 - * 24 - * 3600 - * pygem_prms['constants']['density_ice'] - / pygem_prms['constants']['density_water'] + # Record glacierwide annual mass balance + t_start, t_stop = mbmod.get_step_inds(year) + mb_all.append( + mbmod.glac_wide_massbaltotal[t_start : t_stop + 1].sum() + / mbmod.glacier_area_initial.sum() ) - glac_wide_mb_mwea = ( - mb_mwea * mbmod.glacier_area_initial - ).sum() / mbmod.glacier_area_initial.sum() - mb_all.append(glac_wide_mb_mwea) mbmod.glac_wide_area_annual[-1] = mbmod.glac_wide_area_annual[0] mbmod.glac_wide_volume_annual[-1] = mbmod.glac_wide_volume_annual[0] diag['area_m2'] = mbmod.glac_wide_area_annual @@ -1307,15 +1155,13 @@ def run(list_packed_vars): np.round(np.median(mb_all), 3), ) - # mb_em_mwea = run_emulator_mb(modelprms) - # print(' emulator mb:', np.round(mb_em_mwea,3)) - # mb_em_sims.append(mb_em_mwea) - # Record output for successful runs if successful_run: if args.option_dynamics is not None: if debug: - graphics.plot_modeloutput_section(ev_model, ax=ax, srfls='--') + graphics.plot_modeloutput_section( + ev_model, ax=ax, srfls='--', lnlabel=f'Glacier year {args.sim_endyear + 1}' + ) plt.figure() diag.volume_m3.plot() plt.show() @@ -1359,15 +1205,15 @@ def run(list_packed_vars): ) # RECORD PARAMETERS TO DATASET - output_glac_temp_monthly[:, n_iter] = mbmod.glac_wide_temp - output_glac_prec_monthly[:, n_iter] = mbmod.glac_wide_prec - output_glac_acc_monthly[:, n_iter] = mbmod.glac_wide_acc - output_glac_refreeze_monthly[:, n_iter] = mbmod.glac_wide_refreeze - output_glac_melt_monthly[:, n_iter] = mbmod.glac_wide_melt - output_glac_frontalablation_monthly[:, n_iter] = mbmod.glac_wide_frontalablation - output_glac_massbaltotal_monthly[:, n_iter] = mbmod.glac_wide_massbaltotal - output_glac_runoff_monthly[:, n_iter] = mbmod.glac_wide_runoff - output_glac_snowline_monthly[:, n_iter] = mbmod.glac_wide_snowline + output_glac_temp_steps[:, n_iter] = mbmod.glac_wide_temp + output_glac_prec_steps[:, n_iter] = mbmod.glac_wide_prec + output_glac_acc_steps[:, n_iter] = mbmod.glac_wide_acc + output_glac_refreeze_steps[:, n_iter] = mbmod.glac_wide_refreeze + output_glac_melt_steps[:, n_iter] = mbmod.glac_wide_melt + output_glac_frontalablation_steps[:, n_iter] = mbmod.glac_wide_frontalablation + output_glac_massbaltotal_steps[:, n_iter] = mbmod.glac_wide_massbaltotal + output_glac_runoff_steps[:, n_iter] = mbmod.glac_wide_runoff + output_glac_snowline_steps[:, n_iter] = mbmod.glac_wide_snowline output_glac_area_annual[:, n_iter] = diag.area_m2.values output_glac_mass_annual[:, n_iter] = ( diag.volume_m3.values * pygem_prms['constants']['density_ice'] @@ -1379,12 +1225,12 @@ def run(list_packed_vars): mbmod.glac_wide_volume_change_ignored_annual * pygem_prms['constants']['density_ice'] ) output_glac_ELA_annual[:, n_iter] = mbmod.glac_wide_ELA_annual - output_offglac_prec_monthly[:, n_iter] = mbmod.offglac_wide_prec + output_offglac_prec_steps[:, n_iter] = mbmod.offglac_wide_prec - output_offglac_refreeze_monthly[:, n_iter] = mbmod.offglac_wide_refreeze - output_offglac_melt_monthly[:, n_iter] = mbmod.offglac_wide_melt - output_offglac_snowpack_monthly[:, n_iter] = mbmod.offglac_wide_snowpack - output_offglac_runoff_monthly[:, n_iter] = mbmod.offglac_wide_runoff + output_offglac_refreeze_steps[:, n_iter] = mbmod.offglac_wide_refreeze + output_offglac_melt_steps[:, n_iter] = mbmod.offglac_wide_melt + output_offglac_snowpack_steps[:, n_iter] = mbmod.offglac_wide_snowpack + output_offglac_runoff_steps[:, n_iter] = mbmod.offglac_wide_runoff if output_glac_bin_icethickness_annual is None: output_glac_bin_area_annual_sim = mbmod.glac_bin_area_annual[:, :, np.newaxis] @@ -1425,23 +1271,21 @@ def run(list_packed_vars): output_glac_bin_massbalclim_annual = output_glac_bin_massbalclim_annual_sim[ :, :, np.newaxis ] - output_glac_bin_massbalclim_monthly_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) - output_glac_bin_massbalclim_monthly_sim = mbmod.glac_bin_massbalclim - output_glac_bin_massbalclim_monthly = output_glac_bin_massbalclim_monthly_sim[ - :, :, np.newaxis - ] - # accum - output_glac_bin_acc_monthly_sim = np.zeros(mbmod.bin_acc.shape) - output_glac_bin_acc_monthly_sim = mbmod.bin_acc - output_glac_bin_acc_monthly = output_glac_bin_acc_monthly_sim[:, :, np.newaxis] + output_glac_bin_massbalclim_steps_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) + output_glac_bin_massbalclim_steps_sim = mbmod.glac_bin_massbalclim + output_glac_bin_massbalclim_steps = output_glac_bin_massbalclim_steps_sim[:, :, np.newaxis] + # accumulation + output_glac_bin_acc_steps_sim = np.zeros(mbmod.bin_acc.shape) + output_glac_bin_acc_steps_sim = mbmod.bin_acc + output_glac_bin_acc_steps = output_glac_bin_acc_steps_sim[:, :, np.newaxis] # refreeze - output_glac_bin_refreeze_monthly_sim = np.zeros(mbmod.glac_bin_refreeze.shape) - output_glac_bin_refreeze_monthly_sim = mbmod.glac_bin_refreeze - output_glac_bin_refreeze_monthly = output_glac_bin_refreeze_monthly_sim[:, :, np.newaxis] + output_glac_bin_refreeze_steps_sim = np.zeros(mbmod.glac_bin_refreeze.shape) + output_glac_bin_refreeze_steps_sim = mbmod.glac_bin_refreeze + output_glac_bin_refreeze_steps = output_glac_bin_refreeze_steps_sim[:, :, np.newaxis] # melt - output_glac_bin_melt_monthly_sim = np.zeros(mbmod.glac_bin_melt.shape) - output_glac_bin_melt_monthly_sim = mbmod.glac_bin_melt - output_glac_bin_melt_monthly = output_glac_bin_melt_monthly_sim[:, :, np.newaxis] + output_glac_bin_melt_steps_sim = np.zeros(mbmod.glac_bin_melt.shape) + output_glac_bin_melt_steps_sim = mbmod.glac_bin_melt + output_glac_bin_melt_steps = output_glac_bin_melt_steps_sim[:, :, np.newaxis] else: # Update the latest thickness and volume @@ -1496,35 +1340,35 @@ def run(list_packed_vars): output_glac_bin_massbalclim_annual_sim[:, :, np.newaxis], axis=2, ) - output_glac_bin_massbalclim_monthly_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) - output_glac_bin_massbalclim_monthly_sim = mbmod.glac_bin_massbalclim - output_glac_bin_massbalclim_monthly = np.append( - output_glac_bin_massbalclim_monthly, - output_glac_bin_massbalclim_monthly_sim[:, :, np.newaxis], + output_glac_bin_massbalclim_steps_sim = np.zeros(mbmod.glac_bin_massbalclim.shape) + output_glac_bin_massbalclim_steps_sim = mbmod.glac_bin_massbalclim + output_glac_bin_massbalclim_steps = np.append( + output_glac_bin_massbalclim_steps, + output_glac_bin_massbalclim_steps_sim[:, :, np.newaxis], axis=2, ) - # accum - output_glac_bin_acc_monthly_sim = np.zeros(mbmod.bin_acc.shape) - output_glac_bin_acc_monthly_sim = mbmod.bin_acc - output_glac_bin_acc_monthly = np.append( - output_glac_bin_acc_monthly, - output_glac_bin_acc_monthly_sim[:, :, np.newaxis], + # accumulation + output_glac_bin_acc_steps_sim = np.zeros(mbmod.bin_acc.shape) + output_glac_bin_acc_steps_sim = mbmod.bin_acc + output_glac_bin_acc_steps = np.append( + output_glac_bin_acc_steps, + output_glac_bin_acc_steps_sim[:, :, np.newaxis], axis=2, ) # melt - output_glac_bin_melt_monthly_sim = np.zeros(mbmod.glac_bin_melt.shape) - output_glac_bin_melt_monthly_sim = mbmod.glac_bin_melt - output_glac_bin_melt_monthly = np.append( - output_glac_bin_melt_monthly, - output_glac_bin_melt_monthly_sim[:, :, np.newaxis], + output_glac_bin_melt_steps_sim = np.zeros(mbmod.glac_bin_melt.shape) + output_glac_bin_melt_steps_sim = mbmod.glac_bin_melt + output_glac_bin_melt_steps = np.append( + output_glac_bin_melt_steps, + output_glac_bin_melt_steps_sim[:, :, np.newaxis], axis=2, ) # refreeze - output_glac_bin_refreeze_monthly_sim = np.zeros(mbmod.glac_bin_refreeze.shape) - output_glac_bin_refreeze_monthly_sim = mbmod.glac_bin_refreeze - output_glac_bin_refreeze_monthly = np.append( - output_glac_bin_refreeze_monthly, - output_glac_bin_refreeze_monthly_sim[:, :, np.newaxis], + output_glac_bin_refreeze_steps_sim = np.zeros(mbmod.glac_bin_refreeze.shape) + output_glac_bin_refreeze_steps_sim = mbmod.glac_bin_refreeze + output_glac_bin_refreeze_steps = np.append( + output_glac_bin_refreeze_steps, + output_glac_bin_refreeze_steps_sim[:, :, np.newaxis], axis=2, ) @@ -1537,6 +1381,7 @@ def run(list_packed_vars): output_stats = output.glacierwide_stats( glacier_rgi_table=glacier_rgi_table, dates_table=dates_table, + timestep=pygem_prms['time']['timestep'], nsims=1, sim_climate_name=sim_climate_name, sim_climate_scenario=sim_climate_scenario, @@ -1548,8 +1393,12 @@ def run(list_packed_vars): sim_endyear=args.sim_endyear, option_calibration=args.option_calibration, option_bias_adjustment=args.option_bias_adjustment, + option_dynamics=args.option_dynamics, extra_vars=args.export_extra_vars, ) + base_fn = ( + output_stats.get_fn() + ) # should contain 'SETS' which is later used to replace with the specific iteration for n_iter in range(nsims): # pass model params for iteration and update output dataset model params output_stats.set_modelprms({key: modelprms_all[key][n_iter] for key in modelprms_all}) @@ -1557,69 +1406,54 @@ def run(list_packed_vars): output_stats.create_xr_ds() output_ds_all_stats = output_stats.get_xr_ds() # fill values - output_ds_all_stats['glac_runoff_monthly'].values[0, :] = output_glac_runoff_monthly[ - :, n_iter - ] + output_ds_all_stats['glac_runoff'].values[0, :] = output_glac_runoff_steps[:, n_iter] output_ds_all_stats['glac_area_annual'].values[0, :] = output_glac_area_annual[:, n_iter] output_ds_all_stats['glac_mass_annual'].values[0, :] = output_glac_mass_annual[:, n_iter] output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = output_glac_mass_bsl_annual[ :, n_iter ] output_ds_all_stats['glac_ELA_annual'].values[0, :] = output_glac_ELA_annual[:, n_iter] - output_ds_all_stats['offglac_runoff_monthly'].values[0, :] = output_offglac_runoff_monthly[ - :, n_iter - ] + output_ds_all_stats['offglac_runoff'].values[0, :] = output_offglac_runoff_steps[:, n_iter] if args.export_extra_vars: - output_ds_all_stats['glac_temp_monthly'].values[0, :] = ( - output_glac_temp_monthly[:, n_iter] + 273.15 + output_ds_all_stats['glac_temp'].values[0, :] = ( + output_glac_temp_steps[:, n_iter] + 273.15 ) - output_ds_all_stats['glac_prec_monthly'].values[0, :] = output_glac_prec_monthly[ + output_ds_all_stats['glac_prec'].values[0, :] = output_glac_prec_steps[:, n_iter] + output_ds_all_stats['glac_acc'].values[0, :] = output_glac_acc_steps[:, n_iter] + output_ds_all_stats['glac_refreeze'].values[0, :] = output_glac_refreeze_steps[ :, n_iter ] - output_ds_all_stats['glac_acc_monthly'].values[0, :] = output_glac_acc_monthly[ + output_ds_all_stats['glac_melt'].values[0, :] = output_glac_melt_steps[:, n_iter] + output_ds_all_stats['glac_frontalablation'].values[0, :] = ( + output_glac_frontalablation_steps[:, n_iter] + ) + output_ds_all_stats['glac_massbaltotal'].values[0, :] = output_glac_massbaltotal_steps[ :, n_iter ] - output_ds_all_stats['glac_refreeze_monthly'].values[0, :] = ( - output_glac_refreeze_monthly[:, n_iter] - ) - output_ds_all_stats['glac_melt_monthly'].values[0, :] = output_glac_melt_monthly[ + output_ds_all_stats['glac_snowline'].values[0, :] = output_glac_snowline_steps[ :, n_iter ] - output_ds_all_stats['glac_frontalablation_monthly'].values[0, :] = ( - output_glac_frontalablation_monthly[:, n_iter] - ) - output_ds_all_stats['glac_massbaltotal_monthly'].values[0, :] = ( - output_glac_massbaltotal_monthly[:, n_iter] - ) - output_ds_all_stats['glac_snowline_monthly'].values[0, :] = ( - output_glac_snowline_monthly[:, n_iter] - ) output_ds_all_stats['glac_mass_change_ignored_annual'].values[0, :] = ( output_glac_mass_change_ignored_annual[:, n_iter] ) - output_ds_all_stats['offglac_prec_monthly'].values[0, :] = output_offglac_prec_monthly[ + output_ds_all_stats['offglac_prec'].values[0, :] = output_offglac_prec_steps[:, n_iter] + output_ds_all_stats['offglac_melt'].values[0, :] = output_offglac_melt_steps[:, n_iter] + output_ds_all_stats['offglac_refreeze'].values[0, :] = output_offglac_refreeze_steps[ :, n_iter ] - output_ds_all_stats['offglac_melt_monthly'].values[0, :] = output_offglac_melt_monthly[ + output_ds_all_stats['offglac_snowpack'].values[0, :] = output_offglac_snowpack_steps[ :, n_iter ] - output_ds_all_stats['offglac_refreeze_monthly'].values[0, :] = ( - output_offglac_refreeze_monthly[:, n_iter] - ) - output_ds_all_stats['offglac_snowpack_monthly'].values[0, :] = ( - output_offglac_snowpack_monthly[:, n_iter] - ) # export glacierwide stats for iteration - output_stats.set_fn( - output_stats.get_fn().replace('SETS', f'set{n_iter}') + args.outputfn_sfix + 'all.nc' - ) + output_stats.set_fn(base_fn.replace('SETS', f'set{n_iter}') + args.outputfn_sfix + 'all.nc') output_stats.save_xr_ds() # instantiate dataset for merged simulations output_stats = output.glacierwide_stats( glacier_rgi_table=glacier_rgi_table, dates_table=dates_table, + timestep=pygem_prms['time']['timestep'], nsims=nsims, sim_climate_name=sim_climate_name, sim_climate_scenario=sim_climate_scenario, @@ -1631,6 +1465,7 @@ def run(list_packed_vars): sim_endyear=args.sim_endyear, option_calibration=args.option_calibration, option_bias_adjustment=args.option_bias_adjustment, + option_dynamics=args.option_dynamics, extra_vars=args.export_extra_vars, ) # create and return xarray dataset @@ -1638,128 +1473,95 @@ def run(list_packed_vars): output_ds_all_stats = output_stats.get_xr_ds() # get stats from all simulations which will be stored - output_glac_runoff_monthly_stats = calc_stats_array(output_glac_runoff_monthly) + output_glac_runoff_steps_stats = calc_stats_array(output_glac_runoff_steps) output_glac_area_annual_stats = calc_stats_array(output_glac_area_annual) output_glac_mass_annual_stats = calc_stats_array(output_glac_mass_annual) output_glac_mass_bsl_annual_stats = calc_stats_array(output_glac_mass_bsl_annual) output_glac_ELA_annual_stats = calc_stats_array(output_glac_ELA_annual) - output_offglac_runoff_monthly_stats = calc_stats_array(output_offglac_runoff_monthly) + output_offglac_runoff_steps_stats = calc_stats_array(output_offglac_runoff_steps) if args.export_extra_vars: - output_glac_temp_monthly_stats = calc_stats_array(output_glac_temp_monthly) - output_glac_prec_monthly_stats = calc_stats_array(output_glac_prec_monthly) - output_glac_acc_monthly_stats = calc_stats_array(output_glac_acc_monthly) - output_glac_refreeze_monthly_stats = calc_stats_array(output_glac_refreeze_monthly) - output_glac_melt_monthly_stats = calc_stats_array(output_glac_melt_monthly) - output_glac_frontalablation_monthly_stats = calc_stats_array( - output_glac_frontalablation_monthly - ) - output_glac_massbaltotal_monthly_stats = calc_stats_array(output_glac_massbaltotal_monthly) - output_glac_snowline_monthly_stats = calc_stats_array(output_glac_snowline_monthly) + output_glac_temp_steps_stats = calc_stats_array(output_glac_temp_steps) + output_glac_prec_steps_stats = calc_stats_array(output_glac_prec_steps) + output_glac_acc_steps_stats = calc_stats_array(output_glac_acc_steps) + output_glac_refreeze_steps_stats = calc_stats_array(output_glac_refreeze_steps) + output_glac_melt_steps_stats = calc_stats_array(output_glac_melt_steps) + output_glac_frontalablation_steps_stats = calc_stats_array(output_glac_frontalablation_steps) + output_glac_massbaltotal_steps_stats = calc_stats_array(output_glac_massbaltotal_steps) + output_glac_snowline_steps_stats = calc_stats_array(output_glac_snowline_steps) output_glac_mass_change_ignored_annual_stats = calc_stats_array( output_glac_mass_change_ignored_annual ) - output_offglac_prec_monthly_stats = calc_stats_array(output_offglac_prec_monthly) - output_offglac_melt_monthly_stats = calc_stats_array(output_offglac_melt_monthly) - output_offglac_refreeze_monthly_stats = calc_stats_array(output_offglac_refreeze_monthly) - output_offglac_snowpack_monthly_stats = calc_stats_array(output_offglac_snowpack_monthly) + output_offglac_prec_steps_stats = calc_stats_array(output_offglac_prec_steps) + output_offglac_melt_steps_stats = calc_stats_array(output_offglac_melt_steps) + output_offglac_refreeze_steps_stats = calc_stats_array(output_offglac_refreeze_steps) + output_offglac_snowpack_steps_stats = calc_stats_array(output_offglac_snowpack_steps) # output mean/median from all simulations - output_ds_all_stats['glac_runoff_monthly'].values[0, :] = output_glac_runoff_monthly_stats[:, 0] + output_ds_all_stats['glac_runoff'].values[0, :] = output_glac_runoff_steps_stats[:, 0] output_ds_all_stats['glac_area_annual'].values[0, :] = output_glac_area_annual_stats[:, 0] output_ds_all_stats['glac_mass_annual'].values[0, :] = output_glac_mass_annual_stats[:, 0] output_ds_all_stats['glac_mass_bsl_annual'].values[0, :] = output_glac_mass_bsl_annual_stats[:, 0] output_ds_all_stats['glac_ELA_annual'].values[0, :] = output_glac_ELA_annual_stats[:, 0] - output_ds_all_stats['offglac_runoff_monthly'].values[0, :] = output_offglac_runoff_monthly_stats[ - :, 0 - ] + output_ds_all_stats['offglac_runoff'].values[0, :] = output_offglac_runoff_steps_stats[:, 0] if args.export_extra_vars: - output_ds_all_stats['glac_temp_monthly'].values[0, :] = ( - output_glac_temp_monthly_stats[:, 0] + 273.15 - ) - output_ds_all_stats['glac_prec_monthly'].values[0, :] = output_glac_prec_monthly_stats[:, 0] - output_ds_all_stats['glac_acc_monthly'].values[0, :] = output_glac_acc_monthly_stats[:, 0] - output_ds_all_stats['glac_refreeze_monthly'].values[0, :] = output_glac_refreeze_monthly_stats[ - :, 0 - ] - output_ds_all_stats['glac_melt_monthly'].values[0, :] = output_glac_melt_monthly_stats[:, 0] - output_ds_all_stats['glac_frontalablation_monthly'].values[0, :] = ( - output_glac_frontalablation_monthly_stats[:, 0] + output_ds_all_stats['glac_temp'].values[0, :] = output_glac_temp_steps_stats[:, 0] + 273.15 + output_ds_all_stats['glac_prec'].values[0, :] = output_glac_prec_steps_stats[:, 0] + output_ds_all_stats['glac_acc'].values[0, :] = output_glac_acc_steps_stats[:, 0] + output_ds_all_stats['glac_refreeze'].values[0, :] = output_glac_refreeze_steps_stats[:, 0] + output_ds_all_stats['glac_melt'].values[0, :] = output_glac_melt_steps_stats[:, 0] + output_ds_all_stats['glac_frontalablation'].values[0, :] = ( + output_glac_frontalablation_steps_stats[:, 0] ) - output_ds_all_stats['glac_massbaltotal_monthly'].values[0, :] = ( - output_glac_massbaltotal_monthly_stats[:, 0] - ) - output_ds_all_stats['glac_snowline_monthly'].values[0, :] = output_glac_snowline_monthly_stats[ + output_ds_all_stats['glac_massbaltotal'].values[0, :] = output_glac_massbaltotal_steps_stats[ :, 0 ] + output_ds_all_stats['glac_snowline'].values[0, :] = output_glac_snowline_steps_stats[:, 0] output_ds_all_stats['glac_mass_change_ignored_annual'].values[0, :] = ( output_glac_mass_change_ignored_annual_stats[:, 0] ) - output_ds_all_stats['offglac_prec_monthly'].values[0, :] = output_offglac_prec_monthly_stats[ - :, 0 - ] - output_ds_all_stats['offglac_melt_monthly'].values[0, :] = output_offglac_melt_monthly_stats[ - :, 0 - ] - output_ds_all_stats['offglac_refreeze_monthly'].values[0, :] = ( - output_offglac_refreeze_monthly_stats[:, 0] - ) - output_ds_all_stats['offglac_snowpack_monthly'].values[0, :] = ( - output_offglac_snowpack_monthly_stats[:, 0] - ) + output_ds_all_stats['offglac_prec'].values[0, :] = output_offglac_prec_steps_stats[:, 0] + output_ds_all_stats['offglac_melt'].values[0, :] = output_offglac_melt_steps_stats[:, 0] + output_ds_all_stats['offglac_refreeze'].values[0, :] = output_offglac_refreeze_steps_stats[:, 0] + output_ds_all_stats['offglac_snowpack'].values[0, :] = output_offglac_snowpack_steps_stats[:, 0] # output median absolute deviation if nsims > 1: - output_ds_all_stats['glac_runoff_monthly_mad'].values[0, :] = output_glac_runoff_monthly_stats[ - :, 1 - ] + output_ds_all_stats['glac_runoff_mad'].values[0, :] = output_glac_runoff_steps_stats[:, 1] output_ds_all_stats['glac_area_annual_mad'].values[0, :] = output_glac_area_annual_stats[:, 1] output_ds_all_stats['glac_mass_annual_mad'].values[0, :] = output_glac_mass_annual_stats[:, 1] output_ds_all_stats['glac_mass_bsl_annual_mad'].values[0, :] = ( output_glac_mass_bsl_annual_stats[:, 1] ) output_ds_all_stats['glac_ELA_annual_mad'].values[0, :] = output_glac_ELA_annual_stats[:, 1] - output_ds_all_stats['offglac_runoff_monthly_mad'].values[0, :] = ( - output_offglac_runoff_monthly_stats[:, 1] - ) + output_ds_all_stats['offglac_runoff_mad'].values[0, :] = output_offglac_runoff_steps_stats[:, 1] + output_ds_all_stats['offglac_runoff_mad'].values[0, :] = output_offglac_runoff_steps_stats[:, 1] if args.export_extra_vars: - output_ds_all_stats['glac_temp_monthly_mad'].values[0, :] = output_glac_temp_monthly_stats[ + output_ds_all_stats['glac_temp_mad'].values[0, :] = output_glac_temp_steps_stats[:, 1] + output_ds_all_stats['glac_prec_mad'].values[0, :] = output_glac_prec_steps_stats[:, 1] + output_ds_all_stats['glac_acc_mad'].values[0, :] = output_glac_acc_steps_stats[:, 1] + output_ds_all_stats['glac_refreeze_mad'].values[0, :] = output_glac_refreeze_steps_stats[ :, 1 ] - output_ds_all_stats['glac_prec_monthly_mad'].values[0, :] = output_glac_prec_monthly_stats[ - :, 1 - ] - output_ds_all_stats['glac_acc_monthly_mad'].values[0, :] = output_glac_acc_monthly_stats[ - :, 1 - ] - output_ds_all_stats['glac_refreeze_monthly_mad'].values[0, :] = ( - output_glac_refreeze_monthly_stats[:, 1] + output_ds_all_stats['glac_melt_mad'].values[0, :] = output_glac_melt_steps_stats[:, 1] + output_ds_all_stats['glac_frontalablation_mad'].values[0, :] = ( + output_glac_frontalablation_steps_stats[:, 1] ) - output_ds_all_stats['glac_melt_monthly_mad'].values[0, :] = output_glac_melt_monthly_stats[ + output_ds_all_stats['glac_massbaltotal_mad'].values[0, :] = ( + output_glac_massbaltotal_steps_stats[:, 1] + ) + output_ds_all_stats['glac_snowline_mad'].values[0, :] = output_glac_snowline_steps_stats[ :, 1 ] - output_ds_all_stats['glac_frontalablation_monthly_mad'].values[0, :] = ( - output_glac_frontalablation_monthly_stats[:, 1] - ) - output_ds_all_stats['glac_massbaltotal_monthly_mad'].values[0, :] = ( - output_glac_massbaltotal_monthly_stats[:, 1] - ) - output_ds_all_stats['glac_snowline_monthly_mad'].values[0, :] = ( - output_glac_snowline_monthly_stats[:, 1] - ) output_ds_all_stats['glac_mass_change_ignored_annual_mad'].values[0, :] = ( output_glac_mass_change_ignored_annual_stats[:, 1] ) - output_ds_all_stats['offglac_prec_monthly_mad'].values[0, :] = ( - output_offglac_prec_monthly_stats[:, 1] - ) - output_ds_all_stats['offglac_melt_monthly_mad'].values[0, :] = ( - output_offglac_melt_monthly_stats[:, 1] + output_ds_all_stats['offglac_prec_mad'].values[0, :] = output_offglac_prec_steps_stats[:, 1] + output_ds_all_stats['offglac_melt_mad'].values[0, :] = output_offglac_melt_steps_stats[:, 1] + output_ds_all_stats['offglac_refreeze_mad'].values[0, :] = ( + output_offglac_refreeze_steps_stats[:, 1] ) - output_ds_all_stats['offglac_refreeze_monthly_mad'].values[0, :] = ( - output_offglac_refreeze_monthly_stats[:, 1] - ) - output_ds_all_stats['offglac_snowpack_monthly_mad'].values[0, :] = ( - output_offglac_snowpack_monthly_stats[:, 1] + output_ds_all_stats['offglac_snowpack_mad'].values[0, :] = ( + output_offglac_snowpack_steps_stats[:, 1] ) # export merged netcdf glacierwide stats @@ -1781,6 +1583,7 @@ def run(list_packed_vars): output_binned = output.binned_stats( glacier_rgi_table=glacier_rgi_table, dates_table=dates_table, + timestep=pygem_prms['time']['timestep'], nsims=1, nbins=surface_h_initial.shape[0], binned_components=args.export_binned_components, @@ -1794,7 +1597,11 @@ def run(list_packed_vars): sim_endyear=args.sim_endyear, option_calibration=args.option_calibration, option_bias_adjustment=args.option_bias_adjustment, + option_dynamics=args.option_dynamics, ) + base_fn = ( + output_binned.get_fn() + ) # should contain 'SETS' which is later used to replace with the specific iteration for n_iter in range(nsims): # pass model params for iteration and update output dataset model params output_binned.set_modelprms({key: modelprms_all[key][n_iter] for key in modelprms_all}) @@ -1816,25 +1623,23 @@ def run(list_packed_vars): output_ds_binned_stats['bin_massbalclim_annual'].values[0, :, :] = ( output_glac_bin_massbalclim_annual[:, :, n_iter] ) - output_ds_binned_stats['bin_massbalclim_monthly'].values[0, :, :] = ( - output_glac_bin_massbalclim_monthly[:, :, n_iter] + output_ds_binned_stats['bin_massbalclim'].values[0, :, :] = ( + output_glac_bin_massbalclim_steps[:, :, n_iter] ) if args.export_binned_components: - output_ds_binned_stats['bin_accumulation_monthly'].values[0, :, :] = ( - output_glac_bin_acc_monthly[:, :, n_iter] - ) - output_ds_binned_stats['bin_melt_monthly'].values[0, :, :] = ( - output_glac_bin_melt_monthly[:, :, n_iter] + output_ds_binned_stats['bin_accumulation'].values[0, :, :] = ( + output_glac_bin_acc_steps[:, :, n_iter] ) - output_ds_binned_stats['bin_refreeze_monthly'].values[0, :, :] = ( - output_glac_bin_refreeze_monthly[:, :, n_iter] + output_ds_binned_stats['bin_melt'].values[0, :, :] = output_glac_bin_melt_steps[ + :, :, n_iter + ] + output_ds_binned_stats['bin_refreeze'].values[0, :, :] = ( + output_glac_bin_refreeze_steps[:, :, n_iter] ) # export binned stats for iteration output_binned.set_fn( - output_binned.get_fn().replace('SETS', f'set{n_iter}') - + args.outputfn_sfix - + 'binned.nc' + base_fn.replace('SETS', f'set{n_iter}') + args.outputfn_sfix + 'binned.nc' ) output_binned.save_xr_ds() @@ -1842,6 +1647,7 @@ def run(list_packed_vars): output_binned = output.binned_stats( glacier_rgi_table=glacier_rgi_table, dates_table=dates_table, + timestep=pygem_prms['time']['timestep'], nsims=nsims, nbins=surface_h_initial.shape[0], binned_components=args.export_binned_components, @@ -1855,6 +1661,7 @@ def run(list_packed_vars): sim_endyear=args.sim_endyear, option_calibration=args.option_calibration, option_bias_adjustment=args.option_bias_adjustment, + option_dynamics=args.option_dynamics, ) # create and return xarray dataset output_binned.create_xr_ds() @@ -1875,18 +1682,18 @@ def run(list_packed_vars): output_ds_binned_stats['bin_massbalclim_annual'].values = np.median( output_glac_bin_massbalclim_annual, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats['bin_massbalclim_monthly'].values = np.median( - output_glac_bin_massbalclim_monthly, axis=2 + output_ds_binned_stats['bin_massbalclim'].values = np.median( + output_glac_bin_massbalclim_steps, axis=2 )[np.newaxis, :, :] if args.export_binned_components: - output_ds_binned_stats['bin_accumulation_monthly'].values = np.median( - output_glac_bin_acc_monthly, axis=2 + output_ds_binned_stats['bin_accumulation'].values = np.median( + output_glac_bin_acc_steps, axis=2 )[np.newaxis, :, :] - output_ds_binned_stats['bin_melt_monthly'].values = np.median( - output_glac_bin_melt_monthly, axis=2 - )[np.newaxis, :, :] - output_ds_binned_stats['bin_refreeze_monthly'].values = np.median( - output_glac_bin_refreeze_monthly, axis=2 + output_ds_binned_stats['bin_melt'].values = np.median(output_glac_bin_melt_steps, axis=2)[ + np.newaxis, :, : + ] + output_ds_binned_stats['bin_refreeze'].values = np.median( + output_glac_bin_refreeze_steps, axis=2 )[np.newaxis, :, :] if nsims > 1: output_ds_binned_stats['bin_mass_annual_mad'].values = median_abs_deviation( @@ -1916,11 +1723,6 @@ def run(list_packed_vars): with open(fail_fp + txt_fn_fail, 'w') as text_file: text_file.write(glacier_str + f' failed to complete simulation: {err}') - # Global variables for Spyder development - if args.ncores == 1: - global main_vars - main_vars = inspect.currentframe().f_locals - # %% PARALLEL PROCESSING def main(): diff --git a/pygem/bin/run/run_spinup.py b/pygem/bin/run/run_spinup.py index eeffb779..a3638e96 100644 --- a/pygem/bin/run/run_spinup.py +++ b/pygem/bin/run/run_spinup.py @@ -34,8 +34,11 @@ from pygem.utils._funcs import interp1d_fill_gaps -# get model monthly deltah -def get_elev_change_1d_hat(gdir): +def calculate_elev_change_1d(gdir): + """ + calculate the 1d elevation change from dynamical spinup. + assumes constant flux divergence througohut each model year. + """ # load flowline_diagnostics from spinup f = gdir.get_filepath('fl_diagnostics', filesuffix='_dynamic_spinup_pygem_mb') with xr.open_dataset(f, group='fl_0') as ds_spn: @@ -50,55 +53,65 @@ def get_elev_change_1d_hat(gdir): 0, ) - thickness_m = ds_spn.thickness_m.values.T # glacier thickness [m ice], (nbins, nyears) - - # set any < 0 thickness to nan - thickness_m[thickness_m <= 0] = np.nan - - # climatic mass balance - dotb_monthly = np.repeat(ds_spn.climatic_mb_myr.values.T[:, 1:] / 12, 12, axis=-1) - - # convert to m ice - dotb_monthly = dotb_monthly * (pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice']) - ### to get monthly thickness and mass we require monthly flux divergence ### - # we'll assume the flux divergence is constant througohut the year (is this a good assumption?) - # ie. take annual values and divide by 12 - use numpy repeat to repeat values across 12 months - flux_div_monthly_mmo = np.repeat(-ds_spn.flux_divergence_myr.values.T[:, 1:] / 12, 12, axis=-1) - # get monthly binned change in thickness - delta_h_monthly = dotb_monthly - flux_div_monthly_mmo # [m ice per month] - - # get binned monthly thickness = running thickness change + initial thickness - running_delta_h_monthly = np.cumsum(delta_h_monthly, axis=-1) - h_monthly = running_delta_h_monthly + thickness_m[:, 0][:, np.newaxis] - + ### get monthly elevation change ### + # note, transpose and [:, 1:] indexing to access desired OGGM dataset outputs + # OGGM stores data following (nsteps, nbins) - while PyGEM follows (nbins, nsteps), hence .T operator + # OGGM also stores the 0th model year with np.nan values, hence [:,1:] indexing. Time index 1 corresponds to the output from model year 0-1 + # --- Step 1: convert mass balance from m w.e. to m ice --- + rho_w = pygem_prms['constants']['density_water'] + rho_i = pygem_prms['constants']['density_ice'] + bin_massbalclim_ice = ds_spn.climatic_mb_myr.values.T[:, 1:] * ( + rho_w / rho_i + ) # binned climatic mass balance (nbins, nsteps) + + # --- Step 2: expand annual climatic mass balance and flux divergence to monthly steps --- + # assume the climatic mass balance and flux divergence are constant througohut the year + # ie. take annual values and divide spread uniformly throughout model year + bin_massbalclim_ice_monthly = np.repeat(bin_massbalclim_ice / 12, 12, axis=-1) # [m ice] + bin_flux_divergence_monthly = np.repeat( + -ds_spn.flux_divergence_myr.values.T[:, 1:] / 12, 12, axis=-1 + ) # [m ice] note, oggm flux_divergence_myr is opposite sign of convention, hence negative + + # --- Step 3: compute monthly thickness change --- + bin_delta_thick_monthly = bin_massbalclim_ice_monthly - bin_flux_divergence_monthly # [m ice] + + # --- Step 4: calculate monthly thickness --- + # calculate binned monthly thickness = running thickness change + initial thickness + bin_thick_initial = ds_spn.thickness_m.isel(time=0).values # initial glacier thickness [m ice], (nbins) + running_bin_delta_thick_monthly = np.cumsum(bin_delta_thick_monthly, axis=-1) + bin_thick_monthly = running_bin_delta_thick_monthly + bin_thick_initial[:, np.newaxis] + + # --- Step 5: rebin monthly thickness --- # get surface height at the specified reference year - ref_surface_h = ds_spn.bed_h.values + ds_spn.thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values - - # aggregate model bin thicknesses as desired + ref_surface_height = ds_spn.bed_h.values + ds_spn.thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values + # aggregate model bin thicknesses with warnings.catch_warnings(): warnings.filterwarnings('ignore') - h_monthly = np.column_stack( + bin_thick_monthly = np.column_stack( [ binned_statistic( - x=ref_surface_h, values=x, statistic=np.nanmean, bins=gdir.elev_change_1d['bin_edges'] + x=ref_surface_height, + values=x, + statistic=np.nanmean, + bins=gdir.elev_change_1d['bin_edges'], )[0] - for x in h_monthly.T + for x in bin_thick_monthly.T ] ) - # interpolate over any empty bins - h_monthly_ = np.column_stack([interp1d_fill_gaps(x.copy()) for x in h_monthly.T]) + bin_thick_monthly = np.column_stack([interp1d_fill_gaps(x.copy()) for x in bin_thick_monthly.T]) - # difference desired time steps, return np.nan array for any missing inds - dh = np.column_stack( + # --- Step 6: calculate elevation change --- + elev_change_1d = np.column_stack( [ - h_monthly_[:, j] - h_monthly_[:, i] - if (i is not None and j is not None and 0 <= i < h_monthly_.shape[1] and 0 <= j < h_monthly_.shape[1]) - else np.full(h_monthly_.shape[0], np.nan) - for i, j in gdir.elev_change_1d['model2obs_inds_map'] + bin_thick_monthly[:, tup[1]] - bin_thick_monthly[:, tup[0]] + if tup[0] is not None and tup[1] is not None + else np.full(bin_thick_monthly.shape[0], np.nan) + for tup in gdir.elev_change_1d['model2obs_inds_map'] ] ) - return dh, ds_spn.dis_along_flowline.values, area + + return elev_change_1d, ds_spn.dis_along_flowline.values, area def loss_with_penalty(x, obs, mod, threshold=100, weight=1.0): @@ -290,7 +303,7 @@ def _objective(**kwargs): for start, end in gd.elev_change_1d['dates'] ] - dh_hat, dist, bin_area = get_elev_change_1d_hat(gd) + dh_hat, dist, bin_area = calculate_elev_change_1d(gd) dhdt_hat = dh_hat / gd.elev_change_1d['nyrs'] # plot binned surface area diff --git a/pygem/class_climate.py b/pygem/class_climate.py index 5cb81865..de069ee4 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -166,6 +166,12 @@ def __init__(self, name=str(), sim_climate_scenario=str(), realization=None): # Set parameters for ERA5, ERA-Interim, and CMIP5 netcdf files if self.name == 'ERA5': + # Ensure if using daily data, then including leap years + if pygem_prms['time']['timestep'] == 'daily': + assert pygem_prms['time']['option_leapyear'] == 1, ( + 'option_leapyear must be set to 1 if using daily ERA5 data' + ) + # Variable names self.temp_vn = 't2m' self.tempstd_vn = 't2m_std' @@ -281,6 +287,11 @@ def importGCMfxnearestneighbor_xarray(self, filename, vn, main_glac_rgi): # Import netcdf file data = xr.open_dataset(self.fx_fp + filename) glac_variable = np.zeros(main_glac_rgi.shape[0]) + + # convert longitude from -180—180 to 0—360 + if data.longitude.min() < 0: + data = data.assign_coords(longitude=(data.longitude % 360)) + # If time dimension included, then set the time index (required for ERA Interim, but not for CMIP5 or COAWST) if 'time' in data[vn].coords: time_idx = 0 @@ -599,8 +610,8 @@ def importGCMvarnearestneighbor_xarray( print('Check units of precipitation from GCM is meters per day.') if self.timestep == 'monthly' and self.name != 'COAWST': # Convert from meters per day to meters per month (COAWST data already 'monthly accumulated precipitation') - if 'daysinmonth' in dates_table.columns: - glac_variable_series = glac_variable_series * dates_table['daysinmonth'].values[np.newaxis, :] + if 'days_in_step' in dates_table.columns: + glac_variable_series = glac_variable_series * dates_table['days_in_step'].values[np.newaxis, :] elif vn != self.lr_vn: print('Check units of air temperature or precipitation') diff --git a/pygem/glacierdynamics.py b/pygem/glacierdynamics.py index b4ca6bdb..16f2e910 100755 --- a/pygem/glacierdynamics.py +++ b/pygem/glacierdynamics.py @@ -48,7 +48,6 @@ def __init__( inplace=False, debug=True, option_areaconstant=False, - spinupyears=0, constantarea_years=0, **kwargs, ): @@ -87,12 +86,10 @@ def __init__( ) self.option_areaconstant = option_areaconstant self.constantarea_years = constantarea_years - self.spinupyears = spinupyears self.glac_idx_initial = [fl.thick.nonzero()[0] for fl in flowlines] - self.y0 = 0 + self.y0 = y0 self.is_tidewater = is_tidewater self.water_level = water_level - # widths_t0 = flowlines[0].widths_m # area_v1 = widths_t0 * flowlines[0].dx_meter # print('area v1:', area_v1.sum()) @@ -326,8 +323,13 @@ def run_until_and_store(self, y1, run_path=None, diag_path=None, store_monthly_s def updategeometry(self, year, debug=False): """Update geometry for a given year""" + # get year index + year_idx = self.mb_model.get_year_index(year) + # get time step indices - note indexing should be [t_start:t_stop+1] to include final step in year + t_start, t_stop = self.mb_model.get_step_inds(year) if debug: - print('year:', year) + print('year:', year, f'({year_idx})') + print('time steps:', f'[{t_start}, {t_stop}]') # Loop over flowlines for fl_id, fl in enumerate(self.fls): @@ -338,8 +340,8 @@ def updategeometry(self, year, debug=False): width_t0 = self.fls[fl_id].widths_m.copy() # CONSTANT AREAS - # Mass redistribution ignored for calibration and spinup years (glacier properties constant) - if (self.option_areaconstant) or (year < self.spinupyears) or (year < self.constantarea_years): + # Mass redistribution ignored for calibration years (glacier properties constant) + if (self.option_areaconstant) or (year < self.y0 + self.constantarea_years): # run mass balance glac_bin_massbalclim_annual = self.mb_model.get_annual_mb( heights, fls=self.fls, fl_id=fl_id, year=year, debug=False @@ -380,7 +382,7 @@ def updategeometry(self, year, debug=False): # If frontal ablation more than bin volume, remove entire bin if fa_m3 > vol_last: # Record frontal ablation (m3 w.e.) in mass balance model for output - self.mb_model.glac_bin_frontalablation[last_idx, int(12 * (year + 1) - 1)] = ( + self.mb_model.glac_bin_frontalablation[last_idx, t_stop + 1] = ( vol_last * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] @@ -397,7 +399,7 @@ def updategeometry(self, year, debug=False): section_t0[last_idx] = section_t0[last_idx] - fa_m3 / fl.dx_meter self.fls[fl_id].section = section_t0 # Record frontal ablation(m3 w.e.) - self.mb_model.glac_bin_frontalablation[last_idx, int(12 * (year + 1) - 1)] = ( + self.mb_model.glac_bin_frontalablation[last_idx, t_stop + 1] = ( fa_m3 * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] @@ -433,11 +435,8 @@ def updategeometry(self, year, debug=False): heights, fls=self.fls, fl_id=fl_id, year=year, debug=False ) sec_in_year = ( - self.mb_model.dates_table.loc[12 * year : 12 * (year + 1) - 1, 'daysinmonth'].values.sum() - * 24 - * 3600 + self.mb_model.dates_table.iloc[t_start : t_stop + 1]['days_in_step'].values.sum() * 24 * 3600 ) - # print(' volume change [m3]:', (glac_bin_massbalclim_annual * sec_in_year * # (width_t0 * fl.dx_meter)).sum()) # print(glac_bin_masssbalclim_annual) @@ -469,14 +468,13 @@ def updategeometry(self, year, debug=False): # Record glacier properties (volume [m3], area [m2], thickness [m], width [km]) # record the next year's properties as well # 'year + 1' used so the glacier properties are consistent with mass balance computations - year = int(year) # required to ensure proper indexing with run_until_and_store (10/21/2020) glacier_area = fl.widths_m * fl.dx_meter glacier_area[fl.thick == 0] = 0 - self.mb_model.glac_bin_area_annual[:, year + 1] = glacier_area - self.mb_model.glac_bin_icethickness_annual[:, year + 1] = fl.thick - self.mb_model.glac_bin_width_annual[:, year + 1] = fl.widths_m - self.mb_model.glac_wide_area_annual[year + 1] = glacier_area.sum() - self.mb_model.glac_wide_volume_annual[year + 1] = (fl.section * fl.dx_meter).sum() + self.mb_model.glac_bin_area_annual[:, year_idx + 1] = glacier_area + self.mb_model.glac_bin_icethickness_annual[:, year_idx + 1] = fl.thick + self.mb_model.glac_bin_width_annual[:, year_idx + 1] = fl.widths_m + self.mb_model.glac_wide_area_annual[year_idx + 1] = glacier_area.sum() + self.mb_model.glac_wide_volume_annual[year_idx + 1] = (fl.section * fl.dx_meter).sum() # %% ----- FRONTAL ABLATION ----- def _get_annual_frontalablation(self, heights, year=None, fls=None, fl_id=None, calving_k=None, debug=False): diff --git a/pygem/massbalance.py b/pygem/massbalance.py index 60ebdb1f..20425eb7 100644 --- a/pygem/massbalance.py +++ b/pygem/massbalance.py @@ -51,6 +51,8 @@ def __init__( Parameters ---------- + gdir : class object + Glacier directory using OGGM's structure for a single glacier modelprms : dict Model parameters dictionary (lrgcm, lrglac, precfactor, precgrad, ddfsnow, ddfice, tempsnow, tempchange) glacier_rgi_table : pd.Series @@ -63,6 +65,17 @@ def __init__( option to turn on print statements for development or debugging of code debug_refreeze : Boolean option to turn on print statements for development/debugging of refreezing code + fls : class object + flowlines using OGGM's structure for a glacier + fl_id : integer + flowline id associated with the specific flowlines to index + heights : np.array + elevation bins + inversion_filter : Boolean + option to turn on/off an inversion filter that forces the mass balance profile to be + the same or more positive with increasing elevation + ignore_debris : Boolean + option to ignore the sub-debris melt enhancement factors """ if debug: print('\n\nDEBUGGING MASS BALANCE FUNCTION\n\n') @@ -102,57 +115,57 @@ def __init__( # Variables to store (consider storing in xarray) nbins = self.glacier_area_initial.shape[0] - self.nmonths = self.glacier_gcm_temp.shape[0] + self.nsteps = self.glacier_gcm_temp.shape[0] self.years = sorted(set(self.dates_table.year.values)) self.nyears = len(self.years) # create mapper to get the appropriate year index for child functions self.year_to_index = {year: idx for idx, year in enumerate(self.years)} - self.bin_temp = np.zeros((nbins, self.nmonths)) - self.bin_prec = np.zeros((nbins, self.nmonths)) - self.bin_acc = np.zeros((nbins, self.nmonths)) - self.bin_refreezepotential = np.zeros((nbins, self.nmonths)) - self.bin_refreeze = np.zeros((nbins, self.nmonths)) - self.bin_meltglac = np.zeros((nbins, self.nmonths)) - self.bin_meltsnow = np.zeros((nbins, self.nmonths)) - self.bin_melt = np.zeros((nbins, self.nmonths)) - self.bin_snowpack = np.zeros((nbins, self.nmonths)) - self.snowpack_remaining = np.zeros((nbins, self.nmonths)) - self.glac_bin_refreeze = np.zeros((nbins, self.nmonths)) - self.glac_bin_melt = np.zeros((nbins, self.nmonths)) - self.glac_bin_frontalablation = np.zeros((nbins, self.nmonths)) - self.glac_bin_snowpack = np.zeros((nbins, self.nmonths)) - self.glac_bin_massbalclim = np.zeros((nbins, self.nmonths)) + self.bin_temp = np.zeros((nbins, self.nsteps)) + self.bin_prec = np.zeros((nbins, self.nsteps)) + self.bin_acc = np.zeros((nbins, self.nsteps)) + self.bin_refreezepotential = np.zeros((nbins, self.nsteps)) + self.bin_refreeze = np.zeros((nbins, self.nsteps)) + self.bin_meltglac = np.zeros((nbins, self.nsteps)) + self.bin_meltsnow = np.zeros((nbins, self.nsteps)) + self.bin_melt = np.zeros((nbins, self.nsteps)) + self.bin_snowpack = np.zeros((nbins, self.nsteps)) + self.snowpack_remaining = np.zeros((nbins, self.nsteps)) + self.glac_bin_refreeze = np.zeros((nbins, self.nsteps)) + self.glac_bin_melt = np.zeros((nbins, self.nsteps)) + self.glac_bin_frontalablation = np.zeros((nbins, self.nsteps)) + self.glac_bin_snowpack = np.zeros((nbins, self.nsteps)) + self.glac_bin_massbalclim = np.zeros((nbins, self.nsteps)) self.glac_bin_massbalclim_annual = np.zeros((nbins, self.nyears)) self.glac_bin_surfacetype_annual = np.zeros((nbins, self.nyears + 1)) self.glac_bin_area_annual = np.zeros((nbins, self.nyears + 1)) self.glac_bin_icethickness_annual = np.zeros((nbins, self.nyears + 1)) # Needed for MassRedistributionCurves self.glac_bin_width_annual = np.zeros((nbins, self.nyears + 1)) # Needed for MassRedistributionCurves - self.offglac_bin_prec = np.zeros((nbins, self.nmonths)) - self.offglac_bin_melt = np.zeros((nbins, self.nmonths)) - self.offglac_bin_refreeze = np.zeros((nbins, self.nmonths)) - self.offglac_bin_snowpack = np.zeros((nbins, self.nmonths)) + self.offglac_bin_prec = np.zeros((nbins, self.nsteps)) + self.offglac_bin_melt = np.zeros((nbins, self.nsteps)) + self.offglac_bin_refreeze = np.zeros((nbins, self.nsteps)) + self.offglac_bin_snowpack = np.zeros((nbins, self.nsteps)) self.offglac_bin_area_annual = np.zeros((nbins, self.nyears + 1)) - self.glac_wide_temp = np.zeros(self.nmonths) - self.glac_wide_prec = np.zeros(self.nmonths) - self.glac_wide_acc = np.zeros(self.nmonths) - self.glac_wide_refreeze = np.zeros(self.nmonths) - self.glac_wide_melt = np.zeros(self.nmonths) - self.glac_wide_frontalablation = np.zeros(self.nmonths) - self.glac_wide_massbaltotal = np.zeros(self.nmonths) - self.glac_wide_runoff = np.zeros(self.nmonths) - self.glac_wide_snowline = np.zeros(self.nmonths) + self.glac_wide_temp = np.zeros(self.nsteps) + self.glac_wide_prec = np.zeros(self.nsteps) + self.glac_wide_acc = np.zeros(self.nsteps) + self.glac_wide_refreeze = np.zeros(self.nsteps) + self.glac_wide_melt = np.zeros(self.nsteps) + self.glac_wide_frontalablation = np.zeros(self.nsteps) + self.glac_wide_massbaltotal = np.zeros(self.nsteps) + self.glac_wide_runoff = np.zeros(self.nsteps) + self.glac_wide_snowline = np.zeros(self.nsteps) self.glac_wide_area_annual = np.zeros(self.nyears + 1) self.glac_wide_volume_annual = np.zeros(self.nyears + 1) self.glac_wide_volume_change_ignored_annual = np.zeros(self.nyears) self.glac_wide_ELA_annual = np.zeros(self.nyears + 1) - self.offglac_wide_prec = np.zeros(self.nmonths) - self.offglac_wide_refreeze = np.zeros(self.nmonths) - self.offglac_wide_melt = np.zeros(self.nmonths) - self.offglac_wide_snowpack = np.zeros(self.nmonths) - self.offglac_wide_runoff = np.zeros(self.nmonths) + self.offglac_wide_prec = np.zeros(self.nsteps) + self.offglac_wide_refreeze = np.zeros(self.nsteps) + self.offglac_wide_melt = np.zeros(self.nsteps) + self.offglac_wide_snowpack = np.zeros(self.nsteps) + self.offglac_wide_runoff = np.zeros(self.nsteps) - self.dayspermonth = self.dates_table['daysinmonth'].values + self.days_in_step = self.dates_table['days_in_step'].values self.surfacetype_ddf = np.zeros((nbins)) # Surface type DDF dictionary (manipulate this function for calibration or for each glacier) @@ -182,13 +195,9 @@ def __init__( # refrezee cold content or "potential" refreeze self.rf_cold = np.zeros(nbins) # layer temp of each elev bin for present time step - self.te_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths)) + self.te_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nsteps)) # layer temp of each elev bin for previous time step - self.tl_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nmonths)) - - # Sea level for marine-terminating glaciers - self.sea_level = 0 - rgi_region = int(glacier_rgi_table.RGIId.split('-')[1].split('.')[0]) + self.tl_rf = np.zeros((pygem_prms['mb']['HH2015_rf_opts']['rf_layers'], nbins, self.nsteps)) def get_year_index(self, year): """ @@ -198,6 +207,18 @@ def get_year_index(self, year): assert year in self.years, f'{year} not found in model dates table' return self.year_to_index[year] + def get_step_inds(self, year): + """ + Get time step start and stop indices for the specified model year. + + Both indices are inclusive, so standard Python slicing would use + [t_start : t_stop + 1]. + """ + step_idxs = np.where(self.dates_table.year == int(year))[0] + if step_idxs.size == 0: + raise ValueError(f'Year {year} not found in dates_table.') + return step_idxs[0], step_idxs[-1] + def get_annual_mb( self, heights, @@ -223,13 +244,16 @@ def get_annual_mb( mb : np.array mass balance for each bin [m ice per second] """ + + # assertion to only run with calendar years + assert pygem_prms['climate']['sim_wateryear'] == 'calendar', ( + 'This function is not set up yet to handle non-calendar years' + ) + # get year index year_idx = self.get_year_index(year) - # get start step for 0th month of specified year - year_start_month_idx = 12 * year_idx - # get stop step for specified year - # note, this is 1 greater than the final month which to include - python indexing will not include this month, final month to include of given year is <12*(year_idx+1)-1> - year_stop_month_idx = 12 * (year_idx + 1) + # get time step indices - note indexing should be [t_start:t_stop+1] to include final step in year + t_start, t_stop = self.get_step_inds(year) fl = fls[fl_id] @@ -256,10 +280,10 @@ def get_annual_mb( glac_idx_t0 = glacier_area_t0.nonzero()[0] nbins = heights.shape[0] - nmonths = self.glacier_gcm_temp.shape[0] + nsteps = self.glacier_gcm_temp.shape[0] # Local variables - bin_precsnow = np.zeros((nbins, nmonths)) + bin_precsnow = np.zeros((nbins, nsteps)) # Refreezing specific layers if pygem_prms['mb']['option_refreezing'] == 'HH2015' and year_idx == 0: @@ -269,8 +293,6 @@ def get_annual_mb( refreeze_potential = np.zeros(nbins) if self.glacier_area_initial.sum() > 0: - # if len(glac_idx_t0) > 0: - # Surface type [0=off-glacier, 1=ice, 2=snow, 3=firn, 4=debris] if year_idx == 0: self.surfacetype, self.firnline_idx = self._surfacetypebinsinitial(self.heights) @@ -281,507 +303,482 @@ def get_annual_mb( self.offglac_bin_area_annual[:, year_idx] = glacier_area_initial - glacier_area_t0 offglac_idx = np.where(self.offglac_bin_area_annual[:, year_idx] > 0)[0] - # Functions currently set up for monthly timestep - # only compute mass balance while glacier exists - if pygem_prms['time']['timestep'] == 'monthly': - # if (pygem_prms['time']['timestep'] == 'monthly') and (glac_idx_t0.shape[0] != 0): - - # AIR TEMPERATURE: Downscale the gcm temperature [deg C] to each bin - # Downscale using gcm and glacier lapse rates - # T_bin = T_gcm + lr_gcm * (z_ref - z_gcm) + lr_glac * (z_bin - z_ref) + tempchange - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] = ( - self.glacier_gcm_temp[year_start_month_idx:year_stop_month_idx] - + self.glacier_gcm_lrgcm[year_start_month_idx:year_stop_month_idx] - * ( - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']] - - self.glacier_gcm_elev + # AIR TEMPERATURE: Downscale the gcm temperature [deg C] to each bin + # Downscale using gcm and glacier lapse rates + # T_bin = T_gcm + lr_gcm * (z_ref - z_gcm) + lr_glac * (z_bin - z_ref) + tempchange + self.bin_temp[:, t_start : t_stop + 1] = ( + self.glacier_gcm_temp[t_start : t_stop + 1] + + self.glacier_gcm_lrgcm[t_start : t_stop + 1] + * (self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']] - self.glacier_gcm_elev) + + self.glacier_gcm_lrglac[t_start : t_stop + 1] + * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']])[:, np.newaxis] + + self.modelprms['tbias'] + ) + + # PRECIPITATION/ACCUMULATION: Downscale the precipitation (liquid and solid) to each bin + # Precipitation using precipitation factor and precipitation gradient + # P_bin = P_gcm * prec_factor * (1 + prec_grad * (z_bin - z_ref)) + bin_precsnow[:, t_start : t_stop + 1] = ( + self.glacier_gcm_prec[t_start : t_stop + 1] + * self.modelprms['kp'] + * ( + 1 + + self.modelprms['precgrad'] + * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']]) + )[:, np.newaxis] + ) + # Option to adjust prec of uppermost 25% of glacier for wind erosion and reduced moisture content + if pygem_prms['mb']['option_preclimit'] == 1: + # Elevation range based on all flowlines + raw_min_elev = [] + raw_max_elev = [] + if len(fl.surface_h[fl.widths_m > 0]): + raw_min_elev.append(fl.surface_h[fl.widths_m > 0].min()) + raw_max_elev.append(fl.surface_h[fl.widths_m > 0].max()) + elev_range = np.max(raw_max_elev) - np.min(raw_min_elev) + elev_75 = np.min(raw_min_elev) + 0.75 * (elev_range) + + # If elevation range > 1000 m, apply corrections to uppermost 25% of glacier (Huss and Hock, 2015) + if elev_range > 1000: + # Indices of upper 25% + glac_idx_upper25 = glac_idx_t0[heights[glac_idx_t0] >= elev_75] + # Exponential decay according to elevation difference from the 75% elevation + # prec_upper25 = prec * exp(-(elev_i - elev_75%)/(elev_max- - elev_75%)) + # height at 75% of the elevation + height_75 = heights[glac_idx_upper25].min() + glac_idx_75 = np.where(heights == height_75)[0][0] + # exponential decay + bin_precsnow[glac_idx_upper25, t_start : t_stop + 1] = ( + bin_precsnow[glac_idx_75, t_start : t_stop + 1] + * np.exp( + -1 + * (heights[glac_idx_upper25] - height_75) + / (heights[glac_idx_upper25].max() - heights[glac_idx_upper25].min()) + )[:, np.newaxis] + ) + # Precipitation cannot be less than 87.5% of the maximum accumulation elsewhere on the glacier + # compute max values for each step over glac_idx_t0, compare, and replace if needed + max_values = np.tile( + 0.875 * np.max(bin_precsnow[glac_idx_t0, t_start : t_stop + 1], axis=0), + (8, 1), ) - + self.glacier_gcm_lrglac[year_start_month_idx:year_stop_month_idx] - * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']])[ - :, np.newaxis + uncorrected_values = bin_precsnow[glac_idx_upper25, t_start : t_stop + 1] + corrected_values = np.max(np.stack([uncorrected_values, max_values], axis=0), axis=0) + bin_precsnow[glac_idx_upper25, t_start : t_stop + 1] = corrected_values + + # Separate total precipitation into liquid (bin_prec) and solid (bin_acc) + if pygem_prms['mb']['option_accumulation'] == 1: + # if temperature above threshold, then rain + ( + self.bin_prec[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] > self.modelprms['tsnow_threshold'] ] - + self.modelprms['tbias'] - ) - - # PRECIPITATION/ACCUMULATION: Downscale the precipitation (liquid and solid) to each bin - # Precipitation using precipitation factor and precipitation gradient - # P_bin = P_gcm * prec_factor * (1 + prec_grad * (z_bin - z_ref)) - bin_precsnow[:, year_start_month_idx:year_stop_month_idx] = ( - self.glacier_gcm_prec[year_start_month_idx:year_stop_month_idx] - * self.modelprms['kp'] - * ( - 1 - + self.modelprms['precgrad'] - * (heights - self.glacier_rgi_table.loc[pygem_prms['mb']['option_elev_ref_downscale']]) - )[:, np.newaxis] + ) = bin_precsnow[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] > self.modelprms['tsnow_threshold'] + ] + # if temperature below threshold, then snow + ( + self.bin_acc[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] <= self.modelprms['tsnow_threshold'] + ] + ) = bin_precsnow[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] <= self.modelprms['tsnow_threshold'] + ] + elif pygem_prms['mb']['option_accumulation'] == 2: + # if temperature between min/max, then mix of snow/rain using linear relationship between min/max + self.bin_prec[:, t_start : t_stop + 1] = ( + 0.5 + (self.bin_temp[:, t_start : t_stop + 1] - self.modelprms['tsnow_threshold']) / 2 + ) * bin_precsnow[:, t_start : t_stop + 1] + self.bin_acc[:, t_start : t_stop + 1] = ( + bin_precsnow[:, t_start : t_stop + 1] - self.bin_prec[:, t_start : t_stop + 1] ) - # Option to adjust prec of uppermost 25% of glacier for wind erosion and reduced moisture content - if pygem_prms['mb']['option_preclimit'] == 1: - # Elevation range based on all flowlines - raw_min_elev = [] - raw_max_elev = [] - if len(fl.surface_h[fl.widths_m > 0]): - raw_min_elev.append(fl.surface_h[fl.widths_m > 0].min()) - raw_max_elev.append(fl.surface_h[fl.widths_m > 0].max()) - elev_range = np.max(raw_max_elev) - np.min(raw_min_elev) - elev_75 = np.min(raw_min_elev) + 0.75 * (elev_range) - - # If elevation range > 1000 m, apply corrections to uppermost 25% of glacier (Huss and Hock, 2015) - if elev_range > 1000: - # Indices of upper 25% - glac_idx_upper25 = glac_idx_t0[heights[glac_idx_t0] >= elev_75] - # Exponential decay according to elevation difference from the 75% elevation - # prec_upper25 = prec * exp(-(elev_i - elev_75%)/(elev_max- - elev_75%)) - # height at 75% of the elevation - height_75 = heights[glac_idx_upper25].min() - glac_idx_75 = np.where(heights == height_75)[0][0] - # exponential decay - bin_precsnow[glac_idx_upper25, year_start_month_idx:year_stop_month_idx] = ( - bin_precsnow[glac_idx_75, year_start_month_idx:year_stop_month_idx] - * np.exp( - -1 - * (heights[glac_idx_upper25] - height_75) - / (heights[glac_idx_upper25].max() - heights[glac_idx_upper25].min()) - )[:, np.newaxis] - ) - # Precipitation cannot be less than 87.5% of the maximum accumulation elsewhere on the glacier - for month in range(0, 12): - bin_precsnow[ - glac_idx_upper25[ - ( - bin_precsnow[glac_idx_upper25, month] - < 0.875 * bin_precsnow[glac_idx_t0, month].max() - ) - & (bin_precsnow[glac_idx_upper25, month] != 0) - ], - month, - ] = 0.875 * bin_precsnow[glac_idx_t0, month].max() - - # Separate total precipitation into liquid (bin_prec) and solid (bin_acc) - if pygem_prms['mb']['option_accumulation'] == 1: - # if temperature above threshold, then rain - ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - > self.modelprms['tsnow_threshold'] - ] - ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] > self.modelprms['tsnow_threshold'] + # if temperature above maximum threshold, then all rain + ( + self.bin_prec[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] > self.modelprms['tsnow_threshold'] + 1 ] - # if temperature below threshold, then snow - ( - self.bin_acc[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - <= self.modelprms['tsnow_threshold'] - ] - ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] <= self.modelprms['tsnow_threshold'] + ) = bin_precsnow[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] > self.modelprms['tsnow_threshold'] + 1 + ] + ( + self.bin_acc[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] > self.modelprms['tsnow_threshold'] + 1 ] - elif pygem_prms['mb']['option_accumulation'] == 2: - # if temperature between min/max, then mix of snow/rain using linear relationship between min/max - self.bin_prec[:, year_start_month_idx:year_stop_month_idx] = ( - 0.5 - + ( - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - - self.modelprms['tsnow_threshold'] - ) - / 2 - ) * bin_precsnow[:, year_start_month_idx:year_stop_month_idx] - self.bin_acc[:, year_start_month_idx:year_stop_month_idx] = ( - bin_precsnow[:, year_start_month_idx:year_stop_month_idx] - - self.bin_prec[:, year_start_month_idx:year_stop_month_idx] - ) - # if temperature above maximum threshold, then all rain - ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - > self.modelprms['tsnow_threshold'] + 1 - ] - ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - > self.modelprms['tsnow_threshold'] + 1 + ) = 0 + # if temperature below minimum threshold, then all snow + ( + self.bin_acc[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] <= self.modelprms['tsnow_threshold'] - 1 ] - ( - self.bin_acc[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - > self.modelprms['tsnow_threshold'] + 1 - ] - ) = 0 - # if temperature below minimum threshold, then all snow - ( - self.bin_acc[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - <= self.modelprms['tsnow_threshold'] - 1 - ] - ) = bin_precsnow[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - <= self.modelprms['tsnow_threshold'] - 1 + ) = bin_precsnow[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] <= self.modelprms['tsnow_threshold'] - 1 + ] + ( + self.bin_prec[:, t_start : t_stop + 1][ + self.bin_temp[:, t_start : t_stop + 1] <= self.modelprms['tsnow_threshold'] - 1 ] - ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][ - self.bin_temp[:, year_start_month_idx:year_stop_month_idx] - <= self.modelprms['tsnow_threshold'] - 1 - ] - ) = 0 - - # ENTER MONTHLY LOOP (monthly loop required since surface type changes) - for month in range(0, 12): - # Step is the position as a function of year and month, which improves readability - step = year_start_month_idx + month - - # ACCUMULATION, MELT, REFREEZE, AND CLIMATIC MASS BALANCE - # Snowpack [m w.e.] = snow remaining + new snow - if step == 0: - self.bin_snowpack[:, step] = self.bin_acc[:, step] - else: - self.bin_snowpack[:, step] = self.snowpack_remaining[:, step - 1] + self.bin_acc[:, step] - - # MELT [m w.e.] - # energy available for melt [degC day] - if pygem_prms['mb']['option_ablation'] == 1: - # option 1: energy based on monthly temperature - melt_energy_available = self.bin_temp[:, step] * self.dayspermonth[step] - melt_energy_available[melt_energy_available < 0] = 0 - elif pygem_prms['mb']['option_ablation'] == 2: - # Seed randomness for repeatability, but base it on step to ensure the daily variability is not - # the same for every single time step - np.random.seed(step) - # option 2: monthly temperature superimposed with daily temperature variability - # daily temperature variation in each bin for the monthly timestep - bin_tempstd_daily = np.repeat( - np.random.normal( - loc=0, - scale=self.glacier_gcm_tempstd[step], - size=self.dayspermonth[step], - ).reshape(1, self.dayspermonth[step]), - heights.shape[0], - axis=0, - ) - # daily temperature in each bin for the monthly timestep - bin_temp_daily = self.bin_temp[:, step][:, np.newaxis] + bin_tempstd_daily - # remove negative values - bin_temp_daily[bin_temp_daily < 0] = 0 - # Energy available for melt [degC day] = sum of daily energy available - melt_energy_available = bin_temp_daily.sum(axis=1) - # SNOW MELT [m w.e.] - self.bin_meltsnow[:, step] = self.surfacetype_ddf_dict[2] * melt_energy_available - # snow melt cannot exceed the snow depth - self.bin_meltsnow[self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step] = ( - self.bin_snowpack[self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step] + ) = 0 + + # ENTER TIME STEP LOOP (loop is required since surface type changes) + for step in range(t_start, t_stop): + # ACCUMULATION, MELT, REFREEZE, AND CLIMATIC MASS BALANCE + # Snowpack [m w.e.] = snow remaining + new snow + if step == 0: + self.bin_snowpack[:, step] = self.bin_acc[:, step] + else: + self.bin_snowpack[:, step] = self.snowpack_remaining[:, step - 1] + self.bin_acc[:, step] + + # MELT [m w.e.] + # energy available for melt [degC day] + if pygem_prms['mb']['option_ablation'] == 1: + # option 1: energy based on temperature + melt_energy_available = self.bin_temp[:, step] * self.days_in_step[step] + # assert 1==0, 'here is where we need to change to days per step' + melt_energy_available[melt_energy_available < 0] = 0 + + elif pygem_prms['mb']['option_ablation'] == 2: + assert pygem_prms['time']['timestep'] != 'daily', ( + 'Option 2 for ablation should not be used with daily data' ) - # GLACIER MELT (ice and firn) [m w.e.] - # energy remaining after snow melt [degC day] - melt_energy_available = ( - melt_energy_available - self.bin_meltsnow[:, step] / self.surfacetype_ddf_dict[2] + + # option 2: monthly temperature superimposed with daily temperature variability + # daily temperature variation in each bin for the monthly timestep + # Seed randomness for repeatability, but base it on step to ensure the daily variability is not + # the same for every single time step + np.random.seed(step) + + bin_tempstd_daily = np.repeat( + np.random.normal( + loc=0, + scale=self.glacier_gcm_tempstd[step], + size=self.days_in_step[step], + ).reshape(1, self.days_in_step[step]), + heights.shape[0], + axis=0, ) - # remove low values of energy available caused by rounding errors in the step above - melt_energy_available[abs(melt_energy_available) < pygem_prms['constants']['tolerance']] = 0 - # DDF based on surface type [m w.e. degC-1 day-1] - for surfacetype_idx in self.surfacetype_ddf_dict: - self.surfacetype_ddf[self.surfacetype == surfacetype_idx] = self.surfacetype_ddf_dict[ - surfacetype_idx - ] - # Debris enhancement factors in ablation area (debris in accumulation area would submerge) - if surfacetype_idx == 1 and pygem_prms['mb']['include_debris']: - self.surfacetype_ddf[self.surfacetype == 1] = ( - self.surfacetype_ddf[self.surfacetype == 1] * self.debris_ed[self.surfacetype == 1] - ) - self.bin_meltglac[glac_idx_t0, step] = ( - self.surfacetype_ddf[glac_idx_t0] * melt_energy_available[glac_idx_t0] + # daily temperature in each bin for the given timestep + bin_temp_daily = self.bin_temp[:, step][:, np.newaxis] + bin_tempstd_daily + # remove negative values + bin_temp_daily[bin_temp_daily < 0] = 0 + # Energy available for melt [degC day] = sum of daily energy available + melt_energy_available = bin_temp_daily.sum(axis=1) + # SNOW MELT [m w.e.] + self.bin_meltsnow[:, step] = self.surfacetype_ddf_dict[2] * melt_energy_available + # snow melt cannot exceed the snow depth + self.bin_meltsnow[self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step] = self.bin_snowpack[ + self.bin_meltsnow[:, step] > self.bin_snowpack[:, step], step + ] + # GLACIER MELT (ice and firn) [m w.e.] + # energy remaining after snow melt [degC day] + melt_energy_available = ( + melt_energy_available - self.bin_meltsnow[:, step] / self.surfacetype_ddf_dict[2] + ) + # remove low values of energy available caused by rounding errors in the step above + melt_energy_available[abs(melt_energy_available) < pygem_prms['constants']['tolerance']] = 0 + # DDF based on surface type [m w.e. degC-1 day-1] + for surfacetype_idx in self.surfacetype_ddf_dict: + self.surfacetype_ddf[self.surfacetype == surfacetype_idx] = self.surfacetype_ddf_dict[ + surfacetype_idx + ] + # Debris enhancement factors in ablation area (debris in accumulation area would submerge) + if surfacetype_idx == 1 and pygem_prms['mb']['include_debris']: + self.surfacetype_ddf[self.surfacetype == 1] = ( + self.surfacetype_ddf[self.surfacetype == 1] * self.debris_ed[self.surfacetype == 1] + ) + self.bin_meltglac[glac_idx_t0, step] = ( + self.surfacetype_ddf[glac_idx_t0] * melt_energy_available[glac_idx_t0] + ) + # TOTAL MELT (snow + glacier) + # off-glacier need to include melt of refreeze because there are no glacier dynamics, + # but on-glacier do not need to account for this (simply assume refreeze has same surface type) + self.bin_melt[:, step] = self.bin_meltglac[:, step] + self.bin_meltsnow[:, step] + + # REFREEZING + if pygem_prms['mb']['option_refreezing'] == 'HH2015': + if step > 0: + self.tl_rf[:, :, step] = self.tl_rf[:, :, step - 1] + self.te_rf[:, :, step] = self.te_rf[:, :, step - 1] + + # Refreeze based on heat conduction approach (Huss and Hock 2015) + # refreeze time step (s) + rf_dt = 3600 * 24 * self.days_in_step[step] / pygem_prms['mb']['HH2015_rf_opts']['rf_dsc'] + + if pygem_prms['mb']['HH2015_rf_opts']['option_rf_limit_meltsnow'] == 1: + bin_meltlimit = self.bin_meltsnow.copy() + else: + bin_meltlimit = self.bin_melt.copy() + + # Debug lowest bin + if self.debug_refreeze: + gidx_debug = np.where(heights == heights[glac_idx_t0].min())[0] + + # Loop through each elevation bin of glacier + assert pygem_prms['time']['timestep'] != 'daily', ( + 'must remove the 12 thats hard-coded here - did not do this for HH2015 given the issue' ) - # TOTAL MELT (snow + glacier) - # off-glacier need to include melt of refreeze because there are no glacier dynamics, - # but on-glacier do not need to account for this (simply assume refreeze has same surface type) - self.bin_melt[:, step] = self.bin_meltglac[:, step] + self.bin_meltsnow[:, step] - - # REFREEZING - if pygem_prms['mb']['option_refreezing'] == 'HH2015': - if step > 0: - self.tl_rf[:, :, step] = self.tl_rf[:, :, step - 1] - self.te_rf[:, :, step] = self.te_rf[:, :, step - 1] - - # Refreeze based on heat conduction approach (Huss and Hock 2015) - # refreeze time step (s) - rf_dt = 3600 * 24 * self.dayspermonth[step] / pygem_prms['mb']['HH2015_rf_opts']['rf_dsc'] - - if pygem_prms['mb']['HH2015_rf_opts']['option_rf_limit_meltsnow'] == 1: - bin_meltlimit = self.bin_meltsnow.copy() - else: - bin_meltlimit = self.bin_melt.copy() - # Debug lowest bin - if self.debug_refreeze: - gidx_debug = np.where(heights == heights[glac_idx_t0].min())[0] + for nbin, gidx in enumerate(glac_idx_t0): + # COMPUTE HEAT CONDUCTION - BUILD COLD RESERVOIR + # If no melt, then build up cold reservoir (compute heat conduction) + if self.bin_melt[gidx, step] < pygem_prms['mb']['HH2015_rf_opts']['rf_meltcrit']: + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print( + '\nMonth ' + str(self.dates_table.loc[step, 'month']), + 'Computing heat conduction', + ) - # Loop through each elevation bin of glacier - for nbin, gidx in enumerate(glac_idx_t0): - # COMPUTE HEAT CONDUCTION - BUILD COLD RESERVOIR - # If no melt, then build up cold reservoir (compute heat conduction) - if self.bin_melt[gidx, step] < pygem_prms['mb']['HH2015_rf_opts']['rf_meltcrit']: - if self.debug_refreeze and gidx == gidx_debug and step < 12: - print( - '\nMonth ' + str(self.dates_table.loc[step, 'month']), - 'Computing heat conduction', + # Set refreeze equal to 0 + self.refr[gidx] = 0 + # Loop through multiple iterations to converge on a solution + # -> this will loop through 0, 1, 2 + for h in np.arange(0, pygem_prms['mb']['HH2015_rf_opts']['rf_dsc']): + # Compute heat conduction in layers (loop through rows) + # go from 1 to rf_layers-1 to avoid indexing errors with "j-1" and "j+1" + # "j+1" is set to zero, which is fine for temperate glaciers but inaccurate for + # cold/polythermal glaciers + for j in np.arange( + 1, + pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1, + ): + # Assume temperature of first layer equals air temperature + # assumption probably wrong, but might still work at annual average + # Since next line uses tl_rf for all calculations, set tl_rf[0] to present mean + # monthly air temperature to ensure the present calculations are done with the + # present time step's air temperature + self.tl_rf[0, gidx, step] = self.bin_temp[gidx, step] + # Temperature for each layer + self.te_rf[j, gidx, step] = self.tl_rf[j, gidx, step] + rf_dt * self.rf_layers_k[ + j + ] / self.rf_layers_ch[j] / pygem_prms['mb']['HH2015_rf_opts'][ + 'rf_dz' + ] ** 2 * 0.5 * ( + (self.tl_rf[j - 1, gidx, step] - self.tl_rf[j, gidx, step]) + - (self.tl_rf[j, gidx, step] - self.tl_rf[j + 1, gidx, step]) ) + # Update previous time step + self.tl_rf[:, gidx, step] = self.te_rf[:, gidx, step] - # Set refreeze equal to 0 - self.refr[gidx] = 0 - # Loop through multiple iterations to converge on a solution - # -> this will loop through 0, 1, 2 - for h in np.arange(0, pygem_prms['mb']['HH2015_rf_opts']['rf_dsc']): - # Compute heat conduction in layers (loop through rows) - # go from 1 to rf_layers-1 to avoid indexing errors with "j-1" and "j+1" - # "j+1" is set to zero, which is fine for temperate glaciers but inaccurate for - # cold/polythermal glaciers - for j in np.arange( - 1, - pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1, - ): - # Assume temperature of first layer equals air temperature - # assumption probably wrong, but might still work at annual average - # Since next line uses tl_rf for all calculations, set tl_rf[0] to present mean - # monthly air temperature to ensure the present calculations are done with the - # present time step's air temperature - self.tl_rf[0, gidx, step] = self.bin_temp[gidx, step] - # Temperature for each layer - self.te_rf[j, gidx, step] = self.tl_rf[ - j, gidx, step - ] + rf_dt * self.rf_layers_k[j] / self.rf_layers_ch[j] / pygem_prms['mb'][ - 'HH2015_rf_opts' - ]['rf_dz'] ** 2 * 0.5 * ( - (self.tl_rf[j - 1, gidx, step] - self.tl_rf[j, gidx, step]) - - (self.tl_rf[j, gidx, step] - self.tl_rf[j + 1, gidx, step]) - ) - # Update previous time step - self.tl_rf[:, gidx, step] = self.te_rf[:, gidx, step] + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print( + 'tl_rf:', + ['{:.2f}'.format(x) for x in self.tl_rf[:, gidx, step]], + ) - if self.debug_refreeze and gidx == gidx_debug and step < 12: - print( - 'tl_rf:', - ['{:.2f}'.format(x) for x in self.tl_rf[:, gidx, step]], - ) + # COMPUTE REFREEZING - TAP INTO "COLD RESERVOIR" or potential refreezing + else: + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print( + '\nMonth ' + str(self.dates_table.loc[step, 'month']), + 'Computing refreeze', + ) - # COMPUTE REFREEZING - TAP INTO "COLD RESERVOIR" or potential refreezing + # Refreezing over firn surface + if (self.surfacetype[gidx] == 2) or (self.surfacetype[gidx] == 3): + nlayers = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 + # Refreezing over ice surface else: + # Approximate number of layers of snow on top of ice + smax = np.round( + ( + self.bin_snowpack[gidx, step] / (self.rf_layers_dens[0] / 1000) + + pygem_prms['mb']['HH2015_rf_opts']['pp'] + ) + / pygem_prms['mb']['HH2015_rf_opts']['rf_dz'], + 0, + ) + # if there is very little snow on the ground (SWE > 0.06 m for pp=0.3), + # then still set smax (layers) to 1 + if self.bin_snowpack[gidx, step] > 0 and smax == 0: + smax = 1 + # if no snow on the ground, then set to rf_cold to NoData value + if smax == 0: + self.rf_cold[gidx] = 0 + # if smax greater than the number of layers, set to max number of layers minus 1 + if smax > pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1: + smax = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 + nlayers = int(smax) + # Compute potential refreeze, "cold reservoir", from temperature in each layer + # only calculate potential refreezing first time it starts melting each year + if self.rf_cold[gidx] == 0 and self.tl_rf[:, gidx, step].min() < 0: if self.debug_refreeze and gidx == gidx_debug and step < 12: - print( - '\nMonth ' + str(self.dates_table.loc[step, 'month']), - 'Computing refreeze', + print('calculating potential refreeze from ' + str(nlayers) + ' layers') + + for j in np.arange(0, nlayers): + j += 1 + # units: (degC) * (J K-1 m-3) * (m) * (kg J-1) * (m3 kg-1) + rf_cold_layer = ( + self.tl_rf[j, gidx, step] + * self.rf_layers_ch[j] + * pygem_prms['mb']['HH2015_rf_opts']['rf_dz'] + / pygem_prms['constants']['Lh_rf'] + / pygem_prms['constants']['density_water'] ) + self.rf_cold[gidx] -= rf_cold_layer - # Refreezing over firn surface - if (self.surfacetype[gidx] == 2) or (self.surfacetype[gidx] == 3): - nlayers = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 - # Refreezing over ice surface - else: - # Approximate number of layers of snow on top of ice - smax = np.round( - ( - self.bin_snowpack[gidx, step] / (self.rf_layers_dens[0] / 1000) - + pygem_prms['mb']['HH2015_rf_opts']['pp'] - ) - / pygem_prms['mb']['HH2015_rf_opts']['rf_dz'], - 0, - ) - # if there is very little snow on the ground (SWE > 0.06 m for pp=0.3), - # then still set smax (layers) to 1 - if self.bin_snowpack[gidx, step] > 0 and smax == 0: - smax = 1 - # if no snow on the ground, then set to rf_cold to NoData value - if smax == 0: - self.rf_cold[gidx] = 0 - # if smax greater than the number of layers, set to max number of layers minus 1 - if smax > pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1: - smax = pygem_prms['mb']['HH2015_rf_opts']['rf_layers'] - 1 - nlayers = int(smax) - # Compute potential refreeze, "cold reservoir", from temperature in each layer - # only calculate potential refreezing first time it starts melting each year - if self.rf_cold[gidx] == 0 and self.tl_rf[:, gidx, step].min() < 0: if self.debug_refreeze and gidx == gidx_debug and step < 12: - print('calculating potential refreeze from ' + str(nlayers) + ' layers') - - for j in np.arange(0, nlayers): - j += 1 - # units: (degC) * (J K-1 m-3) * (m) * (kg J-1) * (m3 kg-1) - rf_cold_layer = ( - self.tl_rf[j, gidx, step] - * self.rf_layers_ch[j] - * pygem_prms['mb']['HH2015_rf_opts']['rf_dz'] - / pygem_prms['constants']['Lh_rf'] - / pygem_prms['constants']['density_water'] + print( + 'j:', + j, + 'tl_rf @ j:', + np.round(self.tl_rf[j, gidx, step], 2), + 'ch @ j:', + np.round(self.rf_layers_ch[j], 2), + 'rf_cold_layer @ j:', + np.round(rf_cold_layer, 2), + 'rf_cold @ j:', + np.round(self.rf_cold[gidx], 2), ) - self.rf_cold[gidx] -= rf_cold_layer - - if self.debug_refreeze and gidx == gidx_debug and step < 12: - print( - 'j:', - j, - 'tl_rf @ j:', - np.round(self.tl_rf[j, gidx, step], 2), - 'ch @ j:', - np.round(self.rf_layers_ch[j], 2), - 'rf_cold_layer @ j:', - np.round(rf_cold_layer, 2), - 'rf_cold @ j:', - np.round(self.rf_cold[gidx], 2), - ) - if self.debug_refreeze and gidx == gidx_debug and step < 12: - print('rf_cold:', np.round(self.rf_cold[gidx], 2)) - - # Compute refreezing - # If melt and liquid prec < potential refreeze, then refreeze all melt and liquid prec - if (bin_meltlimit[gidx, step] + self.bin_prec[gidx, step]) < self.rf_cold[gidx]: - self.refr[gidx] = bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] - # otherwise, refreeze equals the potential refreeze - elif self.rf_cold[gidx] > 0: - self.refr[gidx] = self.rf_cold[gidx] - else: - self.refr[gidx] = 0 - - # Track the remaining potential refreeze - self.rf_cold[gidx] -= bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] - # if potential refreeze consumed, set to 0 and set temperature to 0 (temperate firn) - if self.rf_cold[gidx] < 0: - self.rf_cold[gidx] = 0 - self.tl_rf[:, gidx, step] = 0 + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print('rf_cold:', np.round(self.rf_cold[gidx], 2)) + + # Compute refreezing + # If melt and liquid prec < potential refreeze, then refreeze all melt and liquid prec + if (bin_meltlimit[gidx, step] + self.bin_prec[gidx, step]) < self.rf_cold[gidx]: + self.refr[gidx] = bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] + # otherwise, refreeze equals the potential refreeze + elif self.rf_cold[gidx] > 0: + self.refr[gidx] = self.rf_cold[gidx] + else: + self.refr[gidx] = 0 - # Record refreeze - self.bin_refreeze[gidx, step] = self.refr[gidx] + # Track the remaining potential refreeze + self.rf_cold[gidx] -= bin_meltlimit[gidx, step] + self.bin_prec[gidx, step] + # if potential refreeze consumed, set to 0 and set temperature to 0 (temperate firn) + if self.rf_cold[gidx] < 0: + self.rf_cold[gidx] = 0 + self.tl_rf[:, gidx, step] = 0 - if self.debug_refreeze and step < 12 and gidx == gidx_debug: - print( - 'Month ' + str(self.dates_table.loc[step, 'month']), - 'Rf_cold remaining:', - np.round(self.rf_cold[gidx], 2), - 'Snow depth:', - np.round(self.bin_snowpack[glac_idx_t0[nbin], step], 2), - 'Snow melt:', - np.round(self.bin_meltsnow[glac_idx_t0[nbin], step], 2), - 'Rain:', - np.round(self.bin_prec[glac_idx_t0[nbin], step], 2), - 'Rfrz:', - np.round(self.bin_refreeze[gidx, step], 2), - ) + # Record refreeze + self.bin_refreeze[gidx, step] = self.refr[gidx] - elif pygem_prms['mb']['option_refreezing'] == 'Woodward': - # Refreeze based on annual air temperature (Woodward etal. 1997) - # R(m) = (-0.69 * Tair + 0.0096) * 1 m / 100 cm - # calculate annually and place potential refreeze in user defined month - if step % 12 == 0: - bin_temp_annual = annualweightedmean_array( - self.bin_temp[:, year_start_month_idx:year_stop_month_idx], - self.dates_table.iloc[year_start_month_idx:year_stop_month_idx, :], - ) - bin_refreezepotential_annual = (-0.69 * bin_temp_annual + 0.0096) / 100 - # Remove negative refreezing values - bin_refreezepotential_annual[bin_refreezepotential_annual < 0] = 0 - self.bin_refreezepotential[:, step] = bin_refreezepotential_annual - # Reset refreeze potential every year - if self.bin_refreezepotential[:, step].max() > 0: - refreeze_potential = self.bin_refreezepotential[:, step] - - if self.debug_refreeze: + if self.debug_refreeze and step < 12 and gidx == gidx_debug: print( - 'Year ' + str(year) + ' Month ' + str(self.dates_table.loc[step, 'month']), - 'Refreeze potential:', - np.round(refreeze_potential[glac_idx_t0[0]], 3), + 'Month ' + str(self.dates_table.loc[step, 'month']), + 'Rf_cold remaining:', + np.round(self.rf_cold[gidx], 2), 'Snow depth:', - np.round(self.bin_snowpack[glac_idx_t0[0], step], 2), + np.round(self.bin_snowpack[glac_idx_t0[nbin], step], 2), 'Snow melt:', - np.round(self.bin_meltsnow[glac_idx_t0[0], step], 2), + np.round(self.bin_meltsnow[glac_idx_t0[nbin], step], 2), 'Rain:', - np.round(self.bin_prec[glac_idx_t0[0], step], 2), + np.round(self.bin_prec[glac_idx_t0[nbin], step], 2), + 'Rfrz:', + np.round(self.bin_refreeze[gidx, step], 2), ) - # Refreeze [m w.e.] - # refreeze cannot exceed rain and melt (snow & glacier melt) - self.bin_refreeze[:, step] = self.bin_meltsnow[:, step] + self.bin_prec[:, step] - # refreeze cannot exceed snow depth - self.bin_refreeze[ - self.bin_refreeze[:, step] > self.bin_snowpack[:, step], - step, - ] = self.bin_snowpack[ - self.bin_refreeze[:, step] > self.bin_snowpack[:, step], - step, - ] - # refreeze cannot exceed refreeze potential - self.bin_refreeze[self.bin_refreeze[:, step] > refreeze_potential, step] = refreeze_potential[ - self.bin_refreeze[:, step] > refreeze_potential - ] - self.bin_refreeze[ - abs(self.bin_refreeze[:, step]) < pygem_prms['constants']['tolerance'], - step, - ] = 0 - # update refreeze potential - refreeze_potential -= self.bin_refreeze[:, step] - refreeze_potential[abs(refreeze_potential) < pygem_prms['constants']['tolerance']] = 0 - - # SNOWPACK REMAINING [m w.e.] - self.snowpack_remaining[:, step] = self.bin_snowpack[:, step] - self.bin_meltsnow[:, step] - self.snowpack_remaining[ - abs(self.snowpack_remaining[:, step]) < pygem_prms['constants']['tolerance'], + elif pygem_prms['mb']['option_refreezing'] == 'Woodward': + # Refreeze based on annual air temperature (Woodward etal. 1997) + # R(m) = (-0.69 * Tair + 0.0096) * 1 m / 100 cm + # calculate annually and place potential refreeze in user defined month + if step == t_start: + bin_temp_annual = annualweightedmean_array( + self.bin_temp[:, t_start : t_stop + 1], + self.dates_table.iloc[t_start : t_stop + 1, :], + ) + bin_refreezepotential_annual = (-0.69 * bin_temp_annual + 0.0096) / 100 + # Remove negative refreezing values + bin_refreezepotential_annual[bin_refreezepotential_annual < 0] = 0 + self.bin_refreezepotential[:, step] = bin_refreezepotential_annual + # Reset refreeze potential every year + if self.bin_refreezepotential[:, step].max() > 0: + refreeze_potential = self.bin_refreezepotential[:, step] + + if self.debug_refreeze: + print( + self.dates_table.loc[step, 'date'], + 'Refreeze potential:', + np.round(refreeze_potential[glac_idx_t0[50]], 3), + 'Snow depth:', + np.round(self.bin_snowpack[glac_idx_t0[50], step], 2), + 'Snow melt:', + np.round(self.bin_meltsnow[glac_idx_t0[50], step], 2), + 'Rain:', + np.round(self.bin_prec[glac_idx_t0[50], step], 2), + ) + + # Refreeze [m w.e.] + # refreeze cannot exceed rain and melt (snow & glacier melt) + self.bin_refreeze[:, step] = self.bin_meltsnow[:, step] + self.bin_prec[:, step] + # refreeze cannot exceed snow depth + self.bin_refreeze[self.bin_refreeze[:, step] > self.bin_snowpack[:, step], step] = ( + self.bin_snowpack[self.bin_refreeze[:, step] > self.bin_snowpack[:, step], step] + ) + # refreeze cannot exceed refreeze potential + self.bin_refreeze[self.bin_refreeze[:, step] > refreeze_potential, step] = refreeze_potential[ + self.bin_refreeze[:, step] > refreeze_potential + ] + self.bin_refreeze[ + abs(self.bin_refreeze[:, step]) < pygem_prms['constants']['tolerance'], step, ] = 0 + # update refreeze potential + refreeze_potential -= self.bin_refreeze[:, step] + refreeze_potential[abs(refreeze_potential) < pygem_prms['constants']['tolerance']] = 0 + + # SNOWPACK REMAINING [m w.e.] + self.snowpack_remaining[:, step] = self.bin_snowpack[:, step] - self.bin_meltsnow[:, step] + self.snowpack_remaining[ + abs(self.snowpack_remaining[:, step]) < pygem_prms['constants']['tolerance'], + step, + ] = 0 + + # Record values + self.glac_bin_melt[glac_idx_t0, step] = self.bin_melt[glac_idx_t0, step] + self.glac_bin_refreeze[glac_idx_t0, step] = self.bin_refreeze[glac_idx_t0, step] + self.glac_bin_snowpack[glac_idx_t0, step] = self.bin_snowpack[glac_idx_t0, step] + # CLIMATIC MASS BALANCE [m w.e.] + self.glac_bin_massbalclim[glac_idx_t0, step] = ( + self.bin_acc[glac_idx_t0, step] + + self.glac_bin_refreeze[glac_idx_t0, step] + - self.glac_bin_melt[glac_idx_t0, step] + ) - # Record values - self.glac_bin_melt[glac_idx_t0, step] = self.bin_melt[glac_idx_t0, step] - self.glac_bin_refreeze[glac_idx_t0, step] = self.bin_refreeze[glac_idx_t0, step] - self.glac_bin_snowpack[glac_idx_t0, step] = self.bin_snowpack[glac_idx_t0, step] - # CLIMATIC MASS BALANCE [m w.e.] - self.glac_bin_massbalclim[glac_idx_t0, step] = ( - self.bin_acc[glac_idx_t0, step] - + self.glac_bin_refreeze[glac_idx_t0, step] - - self.glac_bin_melt[glac_idx_t0, step] + # OFF-GLACIER ACCUMULATION, MELT, REFREEZE, AND SNOWPACK + if option_areaconstant == False: + # precipitation, refreeze, and snowpack are the same both on- and off-glacier + self.offglac_bin_prec[offglac_idx, step] = self.bin_prec[offglac_idx, step] + self.offglac_bin_refreeze[offglac_idx, step] = self.bin_refreeze[offglac_idx, step] + self.offglac_bin_snowpack[offglac_idx, step] = self.bin_snowpack[offglac_idx, step] + # Off-glacier melt includes both snow melt and melting of refreezing + # (this is not an issue on-glacier because energy remaining melts underlying snow/ice) + # melt of refreezing (assumed to be snow) + self.offglac_meltrefreeze = self.surfacetype_ddf_dict[2] * melt_energy_available + # melt of refreezing cannot exceed refreezing + self.offglac_meltrefreeze[self.offglac_meltrefreeze > self.bin_refreeze[:, step]] = ( + self.bin_refreeze[:, step][self.offglac_meltrefreeze > self.bin_refreeze[:, step]] + ) + # off-glacier melt = snow melt + refreezing melt + self.offglac_bin_melt[offglac_idx, step] = ( + self.bin_meltsnow[offglac_idx, step] + self.offglac_meltrefreeze[offglac_idx] ) - # OFF-GLACIER ACCUMULATION, MELT, REFREEZE, AND SNOWPACK - if option_areaconstant == False: - # precipitation, refreeze, and snowpack are the same both on- and off-glacier - self.offglac_bin_prec[offglac_idx, step] = self.bin_prec[offglac_idx, step] - self.offglac_bin_refreeze[offglac_idx, step] = self.bin_refreeze[offglac_idx, step] - self.offglac_bin_snowpack[offglac_idx, step] = self.bin_snowpack[offglac_idx, step] - # Off-glacier melt includes both snow melt and melting of refreezing - # (this is not an issue on-glacier because energy remaining melts underlying snow/ice) - # melt of refreezing (assumed to be snow) - self.offglac_meltrefreeze = self.surfacetype_ddf_dict[2] * melt_energy_available - # melt of refreezing cannot exceed refreezing - self.offglac_meltrefreeze[self.offglac_meltrefreeze > self.bin_refreeze[:, step]] = ( - self.bin_refreeze[:, step][self.offglac_meltrefreeze > self.bin_refreeze[:, step]] - ) - # off-glacier melt = snow melt + refreezing melt - self.offglac_bin_melt[offglac_idx, step] = ( - self.bin_meltsnow[offglac_idx, step] + self.offglac_meltrefreeze[offglac_idx] - ) - - # ===== RETURN TO ANNUAL LOOP ===== - # SURFACE TYPE (-) - # Annual climatic mass balance [m w.e.] used to determine the surface type - self.glac_bin_massbalclim_annual[:, year_idx] = self.glac_bin_massbalclim[ - :, year_start_month_idx:year_stop_month_idx - ].sum(1) - # Update surface type for each bin - self.surfacetype, firnline_idx = self._surfacetypebinsannual( - self.surfacetype, self.glac_bin_massbalclim_annual, year_idx - ) - # Record binned glacier area - self.glac_bin_area_annual[:, year_idx] = glacier_area_t0 - # Store glacier-wide results - self._convert_glacwide_results( - year_idx, - year_start_month_idx, - year_stop_month_idx, - glacier_area_t0, - heights, - fls=fls, - fl_id=fl_id, - option_areaconstant=option_areaconstant, - ) + # ===== RETURN TO ANNUAL LOOP ===== + # SURFACE TYPE (-) + # Annual climatic mass balance [m w.e.] used to determine the surface type + self.glac_bin_massbalclim_annual[:, year_idx] = self.glac_bin_massbalclim[:, t_start : t_stop + 1].sum(1) + # Update surface type for each bin + self.surfacetype, firnline_idx = self._surfacetypebinsannual( + self.surfacetype, self.glac_bin_massbalclim_annual, year_idx + ) + # Record binned glacier area + self.glac_bin_area_annual[:, year_idx] = glacier_area_t0 + # Store glacier-wide results + self._convert_glacwide_results( + year_idx, + t_start, + t_stop, + glacier_area_t0, + heights, + fls=fls, + fl_id=fl_id, + option_areaconstant=option_areaconstant, + ) - # Mass balance for each bin [m ice per second] - seconds_in_year = self.dayspermonth[year_start_month_idx:year_stop_month_idx].sum() * 24 * 3600 + # Calculate mass balance rate for each bin (convert from [m w.e. yr^-1] to [m ice s^-1]) + seconds_in_year = self.days_in_step[t_start : t_stop + 1].sum() * 24 * 3600 mb = ( - self.glac_bin_massbalclim[:, year_start_month_idx:year_stop_month_idx].sum(1) + self.glac_bin_massbalclim[:, t_start : t_stop + 1].sum(1) * pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice'] / seconds_in_year @@ -799,7 +796,6 @@ def get_annual_mb( mb_filled[(mb_filled == 0) & (heights < height_max)] = mb_min + mb_grad * ( height_min - heights[(mb_filled == 0) & (heights < height_max)] ) - elif len(glac_idx_t0) >= 1 and len(glac_idx_t0) <= 3 and mb.max() <= 0: mb_min = np.min(mb[glac_idx_t0]) height_max = np.max(heights[glac_idx_t0]) @@ -811,8 +807,8 @@ def get_annual_mb( def _convert_glacwide_results( self, year_idx, - year_start_month_idx, - year_stop_month_idx, + t_start, + t_stop, glacier_area, heights, fls=None, @@ -838,7 +834,8 @@ def _convert_glacwide_results( """ # Glacier area glac_idx = glacier_area.nonzero()[0] - glacier_area_monthly = glacier_area[:, np.newaxis].repeat(12, axis=1) + # repeat glacier area for all time steps in year + glacier_area_steps = glacier_area[:, np.newaxis].repeat((t_stop + 1) - t_start, axis=1) # Check if need to adjust for complete removal of the glacier # - needed for accurate runoff calcs and accurate mass balance components @@ -855,7 +852,7 @@ def _convert_glacwide_results( ) # Check annual climatic mass balance (mwea) mb_mwea = ( - glacier_area * self.glac_bin_massbalclim[:, year_start_month_idx:year_stop_month_idx].sum(1) + glacier_area * self.glac_bin_massbalclim[:, t_start : t_stop + 1].sum(1) ).sum() / glacier_area.sum() else: mb_max_loss = 0 @@ -876,82 +873,77 @@ def _convert_glacwide_results( # Glacier-wide area (m2) self.glac_wide_area_annual[year_idx] = glacier_area.sum() # Glacier-wide temperature (degC) - self.glac_wide_temp[year_start_month_idx:year_stop_month_idx] = ( - self.bin_temp[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] + self.glac_wide_temp[t_start : t_stop + 1] = ( + self.bin_temp[:, t_start : t_stop + 1][glac_idx] * glacier_area_steps[glac_idx] ).sum(0) / glacier_area.sum() # Glacier-wide precipitation (m3) - self.glac_wide_prec[year_start_month_idx:year_stop_month_idx] = ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] + self.glac_wide_prec[t_start : t_stop + 1] = ( + self.bin_prec[:, t_start : t_stop + 1][glac_idx] * glacier_area_steps[glac_idx] ).sum(0) # Glacier-wide accumulation (m3 w.e.) - self.glac_wide_acc[year_start_month_idx:year_stop_month_idx] = ( - self.bin_acc[:, year_start_month_idx:year_stop_month_idx][glac_idx] * glacier_area_monthly[glac_idx] + self.glac_wide_acc[t_start : t_stop + 1] = ( + self.bin_acc[:, t_start : t_stop + 1][glac_idx] * glacier_area_steps[glac_idx] ).sum(0) # Glacier-wide refreeze (m3 w.e.) - self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx] = ( - self.glac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][glac_idx] - * glacier_area_monthly[glac_idx] + self.glac_wide_refreeze[t_start : t_stop + 1] = ( + self.glac_bin_refreeze[:, t_start : t_stop + 1][glac_idx] * glacier_area_steps[glac_idx] ).sum(0) # Glacier-wide melt (m3 w.e.) - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.glac_bin_melt[:, year_start_month_idx:year_stop_month_idx][glac_idx] - * glacier_area_monthly[glac_idx] + self.glac_wide_melt[t_start : t_stop + 1] = ( + self.glac_bin_melt[:, t_start : t_stop + 1][glac_idx] * glacier_area_steps[glac_idx] ).sum(0) # Glacier-wide total mass balance (m3 w.e.) - self.glac_wide_massbaltotal[year_start_month_idx:year_stop_month_idx] = ( - self.glac_wide_acc[year_start_month_idx:year_stop_month_idx] - + self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx] - - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] - - self.glac_wide_frontalablation[year_start_month_idx:year_stop_month_idx] + self.glac_wide_massbaltotal[t_start : t_stop + 1] = ( + self.glac_wide_acc[t_start : t_stop + 1] + + self.glac_wide_refreeze[t_start : t_stop + 1] + - self.glac_wide_melt[t_start : t_stop + 1] + - self.glac_wide_frontalablation[t_start : t_stop + 1] ) # If mass loss more negative than glacier mass, reduce melt so glacier completely melts (no excess) if icethickness_t0 is not None and mb_mwea < mb_max_loss: - melt_yr_raw = self.glac_wide_melt[year_start_month_idx:year_stop_month_idx].sum() + melt_yr_raw = self.glac_wide_melt[t_start : t_stop + 1].sum() melt_yr_max = ( self.glac_wide_volume_annual[year_idx] * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] - + self.glac_wide_acc[year_start_month_idx:year_stop_month_idx].sum() - + self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx].sum() + + self.glac_wide_acc[t_start : t_stop + 1].sum() + + self.glac_wide_refreeze[t_start : t_stop + 1].sum() ) melt_frac = melt_yr_max / melt_yr_raw # Update glacier-wide melt (m3 w.e.) - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] * melt_frac - ) + self.glac_wide_melt[t_start : t_stop + 1] = self.glac_wide_melt[t_start : t_stop + 1] * melt_frac # Glacier-wide runoff (m3) - self.glac_wide_runoff[year_start_month_idx:year_stop_month_idx] = ( - self.glac_wide_prec[year_start_month_idx:year_stop_month_idx] - + self.glac_wide_melt[year_start_month_idx:year_stop_month_idx] - - self.glac_wide_refreeze[year_start_month_idx:year_stop_month_idx] + self.glac_wide_runoff[t_start : t_stop + 1] = ( + self.glac_wide_prec[t_start : t_stop + 1] + + self.glac_wide_melt[t_start : t_stop + 1] + - self.glac_wide_refreeze[t_start : t_stop + 1] ) + # Snow line altitude (m a.s.l.) - heights_monthly = heights[:, np.newaxis].repeat(12, axis=1) - snow_mask = np.zeros(heights_monthly.shape) - snow_mask[self.glac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx] > 0] = 1 - heights_monthly_wsnow = heights_monthly * snow_mask - heights_monthly_wsnow[heights_monthly_wsnow == 0] = np.nan + heights_steps = heights[:, np.newaxis].repeat((t_stop + 1) - t_start, axis=1) + snow_mask = np.zeros(heights_steps.shape) + snow_mask[self.glac_bin_snowpack[:, t_start : t_stop + 1] > 0] = 1 + heights_steps_wsnow = heights_steps * snow_mask + heights_steps_wsnow[heights_steps_wsnow == 0] = np.nan heights_change = np.zeros(heights.shape) heights_change[0:-1] = heights[0:-1] - heights[1:] try: - snowline_idx = np.nanargmin(heights_monthly_wsnow, axis=0) - self.glac_wide_snowline[year_start_month_idx:year_stop_month_idx] = ( - heights[snowline_idx] - heights_change[snowline_idx] / 2 - ) + snowline_idx = np.nanargmin(heights_steps_wsnow, axis=0) + self.glac_wide_snowline[t_start : t_stop + 1] = heights[snowline_idx] - heights_change[snowline_idx] / 2 except: - snowline_idx = np.zeros((heights_monthly_wsnow.shape[1])).astype(int) + snowline_idx = np.zeros((heights_steps_wsnow.shape[1])).astype(int) snowline_idx_nan = [] - for ncol in range(heights_monthly_wsnow.shape[1]): - if ~np.isnan(heights_monthly_wsnow[:, ncol]).all(): - snowline_idx[ncol] = np.nanargmin(heights_monthly_wsnow[:, ncol]) + for ncol in range(heights_steps_wsnow.shape[1]): + if ~np.isnan(heights_steps_wsnow[:, ncol]).all(): + snowline_idx[ncol] = np.nanargmin(heights_steps_wsnow[:, ncol]) else: snowline_idx_nan.append(ncol) heights_manual = heights[snowline_idx] - heights_change[snowline_idx] / 2 heights_manual[snowline_idx_nan] = np.nan # this line below causes a potential All-NaN slice encountered issue at some time steps - self.glac_wide_snowline[year_start_month_idx:year_stop_month_idx] = heights_manual + self.glac_wide_snowline[t_start : t_stop + 1] = heights_manual # Equilibrium line altitude (m a.s.l.) ela_mask = np.zeros(heights.shape) @@ -967,33 +959,31 @@ def _convert_glacwide_results( # ===== Off-glacier ==== offglac_idx = np.where(self.offglac_bin_area_annual[:, year_idx] > 0)[0] if option_areaconstant == False and len(offglac_idx) > 0: - offglacier_area_monthly = self.offglac_bin_area_annual[:, year_idx][:, np.newaxis].repeat(12, axis=1) + offglacier_area_steps = self.offglac_bin_area_annual[:, year_idx][:, np.newaxis].repeat( + (t_stop + 1) - t_start, axis=1 + ) # Off-glacier precipitation (m3) - self.offglac_wide_prec[year_start_month_idx:year_stop_month_idx] = ( - self.bin_prec[:, year_start_month_idx:year_stop_month_idx][offglac_idx] - * offglacier_area_monthly[offglac_idx] + self.offglac_wide_prec[t_start : t_stop + 1] = ( + self.bin_prec[:, t_start : t_stop + 1][offglac_idx] * offglacier_area_steps[offglac_idx] ).sum(0) # Off-glacier melt (m3 w.e.) - self.offglac_wide_melt[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_melt[:, year_start_month_idx:year_stop_month_idx][offglac_idx] - * offglacier_area_monthly[offglac_idx] + self.offglac_wide_melt[t_start : t_stop + 1] = ( + self.offglac_bin_melt[:, t_start : t_stop + 1][offglac_idx] * offglacier_area_steps[offglac_idx] ).sum(0) # Off-glacier refreeze (m3 w.e.) - self.offglac_wide_refreeze[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_refreeze[:, year_start_month_idx:year_stop_month_idx][offglac_idx] - * offglacier_area_monthly[offglac_idx] + self.offglac_wide_refreeze[t_start : t_stop + 1] = ( + self.offglac_bin_refreeze[:, t_start : t_stop + 1][offglac_idx] * offglacier_area_steps[offglac_idx] ).sum(0) # Off-glacier runoff (m3) - self.offglac_wide_runoff[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_wide_prec[year_start_month_idx:year_stop_month_idx] - + self.offglac_wide_melt[year_start_month_idx:year_stop_month_idx] - - self.offglac_wide_refreeze[year_start_month_idx:year_stop_month_idx] + self.offglac_wide_runoff[t_start : t_stop + 1] = ( + self.offglac_wide_prec[t_start : t_stop + 1] + + self.offglac_wide_melt[t_start : t_stop + 1] + - self.offglac_wide_refreeze[t_start : t_stop + 1] ) # Off-glacier snowpack (m3 w.e.) - self.offglac_wide_snowpack[year_start_month_idx:year_stop_month_idx] = ( - self.offglac_bin_snowpack[:, year_start_month_idx:year_stop_month_idx][offglac_idx] - * offglacier_area_monthly[offglac_idx] + self.offglac_wide_snowpack[t_start : t_stop + 1] = ( + self.offglac_bin_snowpack[:, t_start : t_stop + 1][offglac_idx] * offglacier_area_steps[offglac_idx] ).sum(0) def ensure_mass_conservation(self, diag): @@ -1010,12 +1000,28 @@ def ensure_mass_conservation(self, diag): total volume change and therefore do not impose limitations like this because they do not estimate the flux divergence. As a result, they may systematically overestimate mass loss compared to OGGM's dynamical model. """ + years = list(np.unique(self.dates_table['year'])) + + # Compute annual volume change and melt values needed for adjustments + vol_change_annual_mbmod = np.zeros(len(years)) + vol_change_annual_mbmod_melt = np.zeros(len(years)) + for nyear, year in enumerate(years): + # get time step indices - note indexing should be [t_start:t_stop+1] to include final step in year + t_start, t_stop = self.get_step_inds(year) + + vol_change_annual_mbmod[nyear] = ( + self.glac_wide_massbaltotal[t_start : t_stop + 1].sum() + * pygem_prms['constants']['density_water'] + / pygem_prms['constants']['density_ice'] + ) + + vol_change_annual_mbmod_melt[nyear] = ( + self.glac_wide_melt[t_start : t_stop + 1].sum() + * pygem_prms['constants']['density_water'] + / pygem_prms['constants']['density_ice'] + ) + # Compute difference between volume change - vol_change_annual_mbmod = ( - self.glac_wide_massbaltotal.reshape(-1, 12).sum(1) - * pygem_prms['constants']['density_water'] - / pygem_prms['constants']['density_ice'] - ) vol_change_annual_diag = np.zeros(vol_change_annual_mbmod.shape) vol_change_annual_diag[0 : diag.volume_m3.values[1:].shape[0]] = ( diag.volume_m3.values[1:] - diag.volume_m3.values[:-1] @@ -1023,11 +1029,6 @@ def ensure_mass_conservation(self, diag): vol_change_annual_dif = vol_change_annual_diag - vol_change_annual_mbmod # Reduce glacier melt by the difference - vol_change_annual_mbmod_melt = ( - self.glac_wide_melt.reshape(-1, 12).sum(1) - * pygem_prms['constants']['density_water'] - / pygem_prms['constants']['density_ice'] - ) vol_change_annual_melt_reduction = np.zeros(vol_change_annual_mbmod.shape) chg_idx = vol_change_annual_mbmod.nonzero()[0] chg_idx_posmbmod = vol_change_annual_mbmod_melt.nonzero()[0] @@ -1037,7 +1038,11 @@ def ensure_mass_conservation(self, diag): 1 - vol_change_annual_dif[chg_idx_melt] / vol_change_annual_mbmod_melt[chg_idx_melt] ) - vol_change_annual_melt_reduction_monthly = np.repeat(vol_change_annual_melt_reduction, 12) + vol_change_annual_melt_reduction_monthly = np.zeros(self.dates_table.shape[0]) + for nyear, year in enumerate(years): + # get time step indices - note indexing should be [t_start:t_stop+1] to include final step in year + t_start, t_stop = self.get_step_inds(year) + vol_change_annual_melt_reduction_monthly[t_start : t_stop + 1] = vol_change_annual_melt_reduction[nyear] # Glacier-wide melt (m3 w.e.) self.glac_wide_melt = self.glac_wide_melt * vol_change_annual_melt_reduction_monthly diff --git a/pygem/output.py b/pygem/output.py index c76005f2..f1cfa7f8 100644 --- a/pygem/output.py +++ b/pygem/output.py @@ -53,6 +53,8 @@ class single_glacier: DataFrame containing metadata and characteristics of the glacier from the Randolph Glacier Inventory. dates_table : pd.DataFrame DataFrame containing the time series of dates associated with the model output. + timestep : str + The time step resolution ('monthly' or 'daily') sim_climate_name : str Name of the General Circulation Model (GCM) used for climate forcing. sim_climate_scenario : str @@ -79,6 +81,7 @@ class single_glacier: glacier_rgi_table: pd.DataFrame dates_table: pd.DataFrame + timestep: str sim_climate_name: str sim_climate_scenario: str realization: str @@ -90,6 +93,7 @@ class single_glacier: sim_endyear: int option_calibration: str option_bias_adjustment: str + option_dynamics: str extra_vars: bool = False def __post_init__(self): @@ -176,8 +180,7 @@ def _set_time_vals(self): self.annual_columns = np.unique(self.dates_table['year'].values)[0 : int(self.dates_table.shape[0] / 12)] elif pygem_prms['climate']['sim_wateryear'] == 'custom': self.year_type = 'custom year' - self.time_values = self.dates_table['date'].values.tolist() - self.time_values = [cftime.DatetimeNoLeap(x.year, x.month, x.day) for x in self.time_values] + self.time_values = [cftime.DatetimeGregorian(x.year, x.month, x.day) for x in self.dates_table['date']] # append additional year to self.year_values to account for mass and area at end of period self.year_values = self.annual_columns self.year_values = np.concatenate((self.year_values, np.array([self.annual_columns[-1] + 1]))) @@ -196,6 +199,8 @@ def _model_params_record(self): self.mdl_params_dict['sim_climate_scenario'] = self.sim_climate_scenario self.mdl_params_dict['option_calibration'] = self.option_calibration self.mdl_params_dict['option_bias_adjustment'] = self.option_bias_adjustment + self.mdl_params_dict['option_dynamics'] = self.option_dynamics + self.mdl_params_dict['timestep'] = self.timestep # record manually defined modelprms if calibration option is None if not self.option_calibration: self._update_modelparams_record() @@ -349,13 +354,13 @@ def _set_outdir(self): def _update_dicts(self): """Update coordinate and attribute dictionaries specific to glacierwide_stats outputs""" - self.output_coords_dict['glac_runoff_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_runoff'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_runoff_monthly'] = { + self.output_attrs_dict['glac_runoff'] = { 'long_name': 'glacier-wide runoff', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'runoff from the glacier terminus, which moves over time', } self.output_coords_dict['glac_area_annual'] = collections.OrderedDict( @@ -394,25 +399,25 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'equilibrium line altitude is the elevation where the climatic mass balance is zero', } - self.output_coords_dict['offglac_runoff_monthly'] = collections.OrderedDict( + self.output_coords_dict['offglac_runoff'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_runoff_monthly'] = { + self.output_attrs_dict['offglac_runoff'] = { 'long_name': 'off-glacier-wide runoff', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'off-glacier runoff from area where glacier no longer exists', } # if nsims > 1, store median-absolute deviation metrics if self.nsims > 1: - self.output_coords_dict['glac_runoff_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_runoff_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_runoff_monthly_mad'] = { + self.output_attrs_dict['glac_runoff_mad'] = { 'long_name': 'glacier-wide runoff median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'runoff from the glacier terminus, which moves over time', } self.output_coords_dict['glac_area_annual_mad'] = collections.OrderedDict( @@ -451,93 +456,93 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'equilibrium line altitude is the elevation where the climatic mass balance is zero', } - self.output_coords_dict['offglac_runoff_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['offglac_runoff_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_runoff_monthly_mad'] = { + self.output_attrs_dict['offglac_runoff_mad'] = { 'long_name': 'off-glacier-wide runoff median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'off-glacier runoff from area where glacier no longer exists', } # optionally store extra variables if self.extra_vars: - self.output_coords_dict['glac_prec_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_prec'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_prec_monthly'] = { + self.output_attrs_dict['glac_prec'] = { 'long_name': 'glacier-wide precipitation (liquid)', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['glac_temp_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_temp'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_temp_monthly'] = { + self.output_attrs_dict['glac_temp'] = { 'standard_name': 'air_temperature', 'long_name': 'glacier-wide mean air temperature', 'units': 'K', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': ( 'each elevation bin is weighted equally to compute the mean temperature, and ' 'bins where the glacier no longer exists due to retreat have been removed' ), } - self.output_coords_dict['glac_acc_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_acc'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_acc_monthly'] = { + self.output_attrs_dict['glac_acc'] = { 'long_name': 'glacier-wide accumulation, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the solid precipitation', } - self.output_coords_dict['glac_refreeze_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_refreeze'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_refreeze_monthly'] = { + self.output_attrs_dict['glac_refreeze'] = { 'long_name': 'glacier-wide refreeze, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['glac_melt_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_melt'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_melt_monthly'] = { + self.output_attrs_dict['glac_melt'] = { 'long_name': 'glacier-wide melt, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['glac_frontalablation_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_frontalablation'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_frontalablation_monthly'] = { + self.output_attrs_dict['glac_frontalablation'] = { 'long_name': 'glacier-wide frontal ablation, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': ( 'mass losses from calving, subaerial frontal melting, sublimation above the ' 'waterline and subaqueous frontal melting below the waterline; positive values indicate mass lost like melt' ), } - self.output_coords_dict['glac_massbaltotal_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_massbaltotal'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_massbaltotal_monthly'] = { + self.output_attrs_dict['glac_massbaltotal'] = { 'long_name': 'glacier-wide total mass balance, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'total mass balance is the sum of the climatic mass balance and frontal ablation', } - self.output_coords_dict['glac_snowline_monthly'] = collections.OrderedDict( + self.output_coords_dict['glac_snowline'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_snowline_monthly'] = { + self.output_attrs_dict['glac_snowline'] = { 'long_name': 'transient snowline altitude above mean sea level', 'units': 'm', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'transient snowline is altitude separating snow from ice/firn', } self.output_coords_dict['glac_mass_change_ignored_annual'] = collections.OrderedDict( @@ -549,119 +554,119 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'glacier mass change ignored due to flux divergence', } - self.output_coords_dict['offglac_prec_monthly'] = collections.OrderedDict( + self.output_coords_dict['offglac_prec'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_prec_monthly'] = { + self.output_attrs_dict['offglac_prec'] = { 'long_name': 'off-glacier-wide precipitation (liquid)', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['offglac_refreeze_monthly'] = collections.OrderedDict( + self.output_coords_dict['offglac_refreeze'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_refreeze_monthly'] = { + self.output_attrs_dict['offglac_refreeze'] = { 'long_name': 'off-glacier-wide refreeze, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['offglac_melt_monthly'] = collections.OrderedDict( + self.output_coords_dict['offglac_melt'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_melt_monthly'] = { + self.output_attrs_dict['offglac_melt'] = { 'long_name': 'off-glacier-wide melt, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only melt of snow and refreeze since off-glacier', } - self.output_coords_dict['offglac_snowpack_monthly'] = collections.OrderedDict( + self.output_coords_dict['offglac_snowpack'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_snowpack_monthly'] = { + self.output_attrs_dict['offglac_snowpack'] = { 'long_name': 'off-glacier-wide snowpack, in water equivalent', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'snow remaining accounting for new accumulation, melt, and refreeze', } # if nsims > 1, store median-absolute deviation metrics if self.nsims > 1: - self.output_coords_dict['glac_prec_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_prec_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_prec_monthly_mad'] = { + self.output_attrs_dict['glac_prec_mad'] = { 'long_name': 'glacier-wide precipitation (liquid) median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['glac_temp_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_temp_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_temp_monthly_mad'] = { + self.output_attrs_dict['glac_temp_mad'] = { 'standard_name': 'air_temperature', 'long_name': 'glacier-wide mean air temperature median absolute deviation', 'units': 'K', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': ( 'each elevation bin is weighted equally to compute the mean temperature, and ' 'bins where the glacier no longer exists due to retreat have been removed' ), } - self.output_coords_dict['glac_acc_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_acc_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_acc_monthly_mad'] = { + self.output_attrs_dict['glac_acc_mad'] = { 'long_name': 'glacier-wide accumulation, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the solid precipitation', } - self.output_coords_dict['glac_refreeze_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_refreeze_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_refreeze_monthly_mad'] = { + self.output_attrs_dict['glac_refreeze_mad'] = { 'long_name': 'glacier-wide refreeze, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['glac_melt_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_melt_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_melt_monthly_mad'] = { + self.output_attrs_dict['glac_melt_mad'] = { 'long_name': 'glacier-wide melt, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['glac_frontalablation_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_frontalablation_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_frontalablation_monthly_mad'] = { + self.output_attrs_dict['glac_frontalablation_mad'] = { 'long_name': 'glacier-wide frontal ablation, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': ( 'mass losses from calving, subaerial frontal melting, sublimation above the ' 'waterline and subaqueous frontal melting below the waterline' ), } - self.output_coords_dict['glac_massbaltotal_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_massbaltotal_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_massbaltotal_monthly_mad'] = { + self.output_attrs_dict['glac_massbaltotal_mad'] = { 'long_name': 'glacier-wide total mass balance, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'total mass balance is the sum of the climatic mass balance and frontal ablation', } - self.output_coords_dict['glac_snowline_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['glac_snowline_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['glac_snowline_monthly_mad'] = { + self.output_attrs_dict['glac_snowline_mad'] = { 'long_name': 'transient snowline above mean sea level median absolute deviation', 'units': 'm', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'transient snowline is altitude separating snow from ice/firn', } self.output_coords_dict['glac_mass_change_ignored_annual_mad'] = collections.OrderedDict( @@ -673,39 +678,39 @@ def _update_dicts(self): 'temporal_resolution': 'annual', 'comment': 'glacier mass change ignored due to flux divergence', } - self.output_coords_dict['offglac_prec_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['offglac_prec_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_prec_monthly_mad'] = { + self.output_attrs_dict['offglac_prec_mad'] = { 'long_name': 'off-glacier-wide precipitation (liquid) median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only the liquid precipitation, solid precipitation excluded', } - self.output_coords_dict['offglac_refreeze_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['offglac_refreeze_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_refreeze_monthly_mad'] = { + self.output_attrs_dict['offglac_refreeze_mad'] = { 'long_name': 'off-glacier-wide refreeze, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, } - self.output_coords_dict['offglac_melt_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['offglac_melt_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_melt_monthly_mad'] = { + self.output_attrs_dict['offglac_melt_mad'] = { 'long_name': 'off-glacier-wide melt, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'only melt of snow and refreeze since off-glacier', } - self.output_coords_dict['offglac_snowpack_monthly_mad'] = collections.OrderedDict( + self.output_coords_dict['offglac_snowpack_mad'] = collections.OrderedDict( [('glac', self.glac_values), ('time', self.time_values)] ) - self.output_attrs_dict['offglac_snowpack_monthly_mad'] = { + self.output_attrs_dict['offglac_snowpack_mad'] = { 'long_name': 'off-glacier-wide snowpack, in water equivalent, median absolute deviation', 'units': 'm3', - 'temporal_resolution': 'monthly', + 'temporal_resolution': self.timestep, 'comment': 'snow remaining accounting for new accumulation, melt, and refreeze', } @@ -824,60 +829,60 @@ def _update_dicts(self): 'comment': 'climatic mass balance is computed before dynamics so can theoretically exceed ice thickness', }, ) - self.output_coords_dict['bin_massbalclim_monthly'] = collections.OrderedDict( + self.output_coords_dict['bin_massbalclim'] = collections.OrderedDict( [ ('glac', self.glac_values), ('bin', self.bin_values), ('time', self.time_values), ] ) - self.output_attrs_dict['bin_massbalclim_monthly'] = { - 'long_name': 'binned monthly climatic mass balance, in water equivalent', + self.output_attrs_dict['bin_massbalclim'] = { + 'long_name': 'binned climatic mass balance, in water equivalent', 'units': 'm', - 'temporal_resolution': 'monthly', - 'comment': 'monthly climatic mass balance from the PyGEM mass balance module', + 'temporal_resolution': self.timestep, + 'comment': 'climatic mass balance from the PyGEM mass balance module', } # optionally store binned mass balance components if self.binned_components: - self.output_coords_dict['bin_accumulation_monthly'] = collections.OrderedDict( + self.output_coords_dict['bin_accumulation'] = collections.OrderedDict( [ ('glac', self.glac_values), ('bin', self.bin_values), ('time', self.time_values), ] ) - self.output_attrs_dict['bin_accumulation_monthly'] = { - 'long_name': 'binned monthly accumulation, in water equivalent', + self.output_attrs_dict['bin_accumulation'] = { + 'long_name': 'binned accumulation, in water equivalent', 'units': 'm', - 'temporal_resolution': 'monthly', - 'comment': 'monthly accumulation from the PyGEM mass balance module', + 'temporal_resolution': self.timestep, + 'comment': 'accumulation from the PyGEM mass balance module', } - self.output_coords_dict['bin_melt_monthly'] = collections.OrderedDict( + self.output_coords_dict['bin_melt'] = collections.OrderedDict( [ ('glac', self.glac_values), ('bin', self.bin_values), ('time', self.time_values), ] ) - self.output_attrs_dict['bin_melt_monthly'] = { - 'long_name': 'binned monthly melt, in water equivalent', + self.output_attrs_dict['bin_melt'] = { + 'long_name': 'binned melt, in water equivalent', 'units': 'm', - 'temporal_resolution': 'monthly', - 'comment': 'monthly melt from the PyGEM mass balance module', + 'temporal_resolution': self.timestep, + 'comment': 'melt from the PyGEM mass balance module', } - self.output_coords_dict['bin_refreeze_monthly'] = collections.OrderedDict( + self.output_coords_dict['bin_refreeze'] = collections.OrderedDict( [ ('glac', self.glac_values), ('bin', self.bin_values), ('time', self.time_values), ] ) - self.output_attrs_dict['bin_refreeze_monthly'] = { - 'long_name': 'binned monthly refreeze, in water equivalent', + self.output_attrs_dict['bin_refreeze'] = { + 'long_name': 'binned refreeze, in water equivalent', 'units': 'm', - 'temporal_resolution': 'monthly', - 'comment': 'monthly refreeze from the PyGEM mass balance module', + 'temporal_resolution': self.timestep, + 'comment': 'refreeze from the PyGEM mass balance module', } # if nsims > 1, store median-absolute deviation metrics diff --git a/pygem/plot/graphics.py b/pygem/plot/graphics.py index 74f0bbba..103725b3 100644 --- a/pygem/plot/graphics.py +++ b/pygem/plot/graphics.py @@ -18,7 +18,15 @@ from pygem.utils.stats import effective_n -def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): +def plot_modeloutput_section( + model=None, + ax=None, + title='', + lnlabel=None, + legendon=True, + lgdkwargs={'loc': 'upper right', 'fancybox': False, 'borderaxespad': 0, 'handlelength': 1}, + **kwargs, +): """Plots the result of the model output along the flowline. A paired down version of OGGMs graphics.plot_modeloutput_section() @@ -40,15 +48,12 @@ def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): ax = fig.add_axes([0.07, 0.08, 0.7, 0.84]) else: fig = plt.gcf() + # get n lines plotted on figure + nlines = len(plt.gca().get_lines()) - # Compute area histo - area = np.array([]) height = np.array([]) bed = np.array([]) for cls in fls: - a = cls.widths_m * cls.dx_meter * 1e-6 - a = np.where(cls.thick > 0, a, 0) - area = np.concatenate((area, a)) height = np.concatenate((height, cls.surface_h)) bed = np.concatenate((bed, cls.bed_h)) ylim = [bed.min(), height.max()] @@ -57,8 +62,11 @@ def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): cls = fls[-1] x = np.arange(cls.nx) * cls.dx * cls.map_dx - # Plot the bed - ax.plot(x, cls.bed_h, color='k', linewidth=2.5, label='Bed (Parab.)') + if nlines == 0: + if getattr(model, 'do_calving', False): + ax.hlines(model.water_level, x[0], x[-1], linestyles=':', label='Water level', color='C0') + # Plot the bed + ax.plot(x, cls.bed_h, color='k', linewidth=2.5, label='Bed (Parab.)') # Plot glacier t1 = cls.thick[:-2] @@ -77,7 +85,7 @@ def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): else: srfls = '-' - ax.plot(x, cls.surface_h, color=srfcolor, linewidth=2, ls=srfls, label='Glacier') + ax.plot(x, cls.surface_h, color=srfcolor, linewidth=2, ls=srfls, label=lnlabel) # Plot tributaries for i, inflow in zip(cls.inflow_indices, cls.inflows): @@ -99,16 +107,14 @@ def plot_modeloutput_section(model=None, ax=None, title='', **kwargs): markeredgecolor='k', label='Tributary (inactive)', ) - if getattr(model, 'do_calving', False): - ax.hlines(model.water_level, x[0], x[-1], linestyles=':', color='C0') ax.set_ylim(ylim) - ax.spines['top'].set_color('none') ax.xaxis.set_ticks_position('bottom') ax.set_xlabel('Distance along flowline (m)') ax.set_ylabel('Altitude (m)') - + if legendon: + ax.legend(**lgdkwargs) # Title ax.set_title(title, loc='left') diff --git a/pygem/pygem_modelsetup.py b/pygem/pygem_modelsetup.py index 9524b878..18a7bb33 100755 --- a/pygem/pygem_modelsetup.py +++ b/pygem/pygem_modelsetup.py @@ -80,30 +80,30 @@ def datesmodelrun( # Select attributes of DateTimeIndex (dt.year, dt.month, and dt.daysinmonth) dates_table['year'] = dates_table['date'].dt.year dates_table['month'] = dates_table['date'].dt.month - dates_table['daysinmonth'] = dates_table['date'].dt.daysinmonth + dates_table['days_in_step'] = dates_table['date'].dt.daysinmonth dates_table['timestep'] = np.arange(len(dates_table['date'])) # Set date as index dates_table.set_index('timestep', inplace=True) # Remove leap year days if user selected this with option_leapyear if pygem_prms['time']['option_leapyear'] == 0: - mask1 = dates_table['daysinmonth'] == 29 - dates_table.loc[mask1, 'daysinmonth'] = 28 + mask1 = dates_table['days_in_step'] == 29 + dates_table.loc[mask1, 'days_in_step'] = 28 elif pygem_prms['time']['timestep'] == 'daily': # Automatically generate daily (freq = 'D') dates - dates_table = pd.DataFrame({'date': pd.date_range(startdate, enddate, freq='D')}) + dates_table = pd.DataFrame({'date': pd.date_range(startdate, enddate, freq='D', unit='s')}) # Extract attributes for dates_table dates_table['year'] = dates_table['date'].dt.year dates_table['month'] = dates_table['date'].dt.month dates_table['day'] = dates_table['date'].dt.day - dates_table['daysinmonth'] = dates_table['date'].dt.daysinmonth + dates_table['days_in_step'] = 1 dates_table['timestep'] = np.arange(len(dates_table['date'])) # Set date as index dates_table.set_index('timestep', inplace=True) # Remove leap year days if user selected this with option_leapyear if pygem_prms['time']['option_leapyear'] == 0: - # First, change 'daysinmonth' number - mask1 = dates_table['daysinmonth'] == 29 - dates_table.loc[mask1, 'daysinmonth'] = 28 + # First, change 'days_in_step' number + mask1 = dates_table['days_in_step'] == 29 + dates_table.loc[mask1, 'days_in_step'] = 28 # Next, remove the 29th days from the dates mask2 = (dates_table['month'] == 2) & (dates_table['day'] == 29) dates_table.drop(dates_table[mask2].index, inplace=True) @@ -135,52 +135,6 @@ def datesmodelrun( return dates_table -def daysinmonth(year, month): - """ - Return days in month based on the month and year - - Parameters - ---------- - year : str - month : str - - Returns - ------- - integer of the days in the month - """ - if year % 4 == 0: - daysinmonth_dict = { - 1: 31, - 2: 29, - 3: 31, - 4: 30, - 5: 31, - 6: 30, - 7: 31, - 8: 31, - 9: 30, - 10: 31, - 11: 30, - 12: 31, - } - else: - daysinmonth_dict = { - 1: 31, - 2: 28, - 3: 31, - 4: 30, - 5: 31, - 6: 30, - 7: 31, - 8: 31, - 9: 30, - 10: 31, - 11: 30, - 12: 31, - } - return daysinmonth_dict[month] - - def hypsometrystats(hyps_table, thickness_table): """Calculate the volume and mean associated with the hypsometry data. diff --git a/pygem/setup/config.py b/pygem/setup/config.py index c60f520c..b6bf47e4 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -131,7 +131,7 @@ def _validate_config(self, config): 'setup.rgi_region02': (str, type(None)), 'setup.glac_no_skip': (list, type(None)), 'setup.glac_no': (list, type(None)), - 'setup.min_glac_area_km2': int, + 'setup.min_glac_area_km2': (int, float), 'setup.include_landterm': bool, 'setup.include_laketerm': bool, 'setup.include_tidewater': bool, @@ -176,21 +176,21 @@ def _validate_config(self, config): 'calib.option_calibration': str, 'calib.priors_reg_fn': str, 'calib.HH2015_params': dict, - 'calib.HH2015_params.tbias_init': int, - 'calib.HH2015_params.tbias_step': int, - 'calib.HH2015_params.kp_init': float, - 'calib.HH2015_params.kp_bndlow': float, - 'calib.HH2015_params.kp_bndhigh': int, - 'calib.HH2015_params.ddfsnow_init': float, - 'calib.HH2015_params.ddfsnow_bndlow': float, - 'calib.HH2015_params.ddfsnow_bndhigh': float, + 'calib.HH2015_params.tbias_init': (int, float), + 'calib.HH2015_params.tbias_step': (int, float), + 'calib.HH2015_params.kp_init': (int, float), + 'calib.HH2015_params.kp_bndlow': (int, float), + 'calib.HH2015_params.kp_bndhigh': (int, float), + 'calib.HH2015_params.ddfsnow_init': (int, float), + 'calib.HH2015_params.ddfsnow_bndlow': (int, float), + 'calib.HH2015_params.ddfsnow_bndhigh': (int, float), 'calib.HH2015mod_params': dict, - 'calib.HH2015mod_params.tbias_init': int, - 'calib.HH2015mod_params.tbias_step': float, - 'calib.HH2015mod_params.kp_init': int, - 'calib.HH2015mod_params.kp_bndlow': float, - 'calib.HH2015mod_params.kp_bndhigh': int, - 'calib.HH2015mod_params.ddfsnow_init': float, + 'calib.HH2015mod_params.tbias_init': (int, float), + 'calib.HH2015mod_params.tbias_step': (int, float), + 'calib.HH2015mod_params.kp_init': (int, float), + 'calib.HH2015mod_params.kp_bndlow': (int, float), + 'calib.HH2015mod_params.kp_bndhigh': (int, float), + 'calib.HH2015mod_params.ddfsnow_init': (int, float), 'calib.HH2015mod_params.method_opt': str, 'calib.HH2015mod_params.params2opt': list, 'calib.HH2015mod_params.ftol_opt': float, @@ -199,22 +199,22 @@ def _validate_config(self, config): 'calib.emulator_params.emulator_sims': int, 'calib.emulator_params.overwrite_em_sims': bool, 'calib.emulator_params.opt_hh2015_mod': bool, - 'calib.emulator_params.tbias_step': float, - 'calib.emulator_params.tbias_init': int, - 'calib.emulator_params.kp_init': int, - 'calib.emulator_params.kp_bndlow': float, - 'calib.emulator_params.kp_bndhigh': int, - 'calib.emulator_params.ddfsnow_init': float, + 'calib.emulator_params.tbias_step': (int, float), + 'calib.emulator_params.tbias_init': (int, float), + 'calib.emulator_params.kp_init': (int, float), + 'calib.emulator_params.kp_bndlow': (int, float), + 'calib.emulator_params.kp_bndhigh': (int, float), + 'calib.emulator_params.ddfsnow_init': (int, float), 'calib.emulator_params.option_areaconstant': bool, 'calib.emulator_params.tbias_disttype': str, - 'calib.emulator_params.tbias_sigma': int, - 'calib.emulator_params.kp_gamma_alpha': int, - 'calib.emulator_params.kp_gamma_beta': int, + 'calib.emulator_params.tbias_sigma': (int, float), + 'calib.emulator_params.kp_gamma_alpha': (int, float), + 'calib.emulator_params.kp_gamma_beta': (int, float), 'calib.emulator_params.ddfsnow_disttype': str, - 'calib.emulator_params.ddfsnow_mu': float, - 'calib.emulator_params.ddfsnow_sigma': float, - 'calib.emulator_params.ddfsnow_bndlow': int, - 'calib.emulator_params.ddfsnow_bndhigh': float, + 'calib.emulator_params.ddfsnow_mu': (int, float), + 'calib.emulator_params.ddfsnow_sigma': (int, float), + 'calib.emulator_params.ddfsnow_bndlow': (int, float), + 'calib.emulator_params.ddfsnow_bndhigh': (int, float), 'calib.emulator_params.method_opt': str, 'calib.emulator_params.params2opt': list, 'calib.emulator_params.ftol_opt': float, @@ -222,33 +222,33 @@ def _validate_config(self, config): 'calib.MCMC_params': dict, 'calib.MCMC_params.option_use_emulator': bool, 'calib.MCMC_params.emulator_sims': int, - 'calib.MCMC_params.tbias_step': float, - 'calib.MCMC_params.tbias_stepsmall': float, + 'calib.MCMC_params.tbias_step': (int, float), + 'calib.MCMC_params.tbias_stepsmall': (int, float), 'calib.MCMC_params.option_areaconstant': bool, - 'calib.MCMC_params.mcmc_step': float, + 'calib.MCMC_params.mcmc_step': (int, float), 'calib.MCMC_params.n_chains': int, 'calib.MCMC_params.mcmc_sample_no': int, 'calib.MCMC_params.mcmc_burn_pct': int, 'calib.MCMC_params.thin_interval': int, 'calib.MCMC_params.ddfsnow_disttype': str, - 'calib.MCMC_params.ddfsnow_mu': float, - 'calib.MCMC_params.ddfsnow_sigma': float, - 'calib.MCMC_params.ddfsnow_bndlow': int, - 'calib.MCMC_params.ddfsnow_bndhigh': float, + 'calib.MCMC_params.ddfsnow_mu': (int, float), + 'calib.MCMC_params.ddfsnow_sigma': (int, float), + 'calib.MCMC_params.ddfsnow_bndlow': (int, float), + 'calib.MCMC_params.ddfsnow_bndhigh': (int, float), 'calib.MCMC_params.kp_disttype': str, 'calib.MCMC_params.tbias_disttype': str, - 'calib.MCMC_params.tbias_mu': int, - 'calib.MCMC_params.tbias_sigma': int, - 'calib.MCMC_params.tbias_bndlow': int, - 'calib.MCMC_params.tbias_bndhigh': int, - 'calib.MCMC_params.kp_gamma_alpha': int, - 'calib.MCMC_params.kp_gamma_beta': int, - 'calib.MCMC_params.kp_lognorm_mu': int, - 'calib.MCMC_params.kp_lognorm_tau': int, - 'calib.MCMC_params.kp_mu': int, - 'calib.MCMC_params.kp_sigma': float, - 'calib.MCMC_params.kp_bndlow': float, - 'calib.MCMC_params.kp_bndhigh': float, + 'calib.MCMC_params.tbias_mu': (int, float), + 'calib.MCMC_params.tbias_sigma': (int, float), + 'calib.MCMC_params.tbias_bndlow': (int, float), + 'calib.MCMC_params.tbias_bndhigh': (int, float), + 'calib.MCMC_params.kp_gamma_alpha': (int, float), + 'calib.MCMC_params.kp_gamma_beta': (int, float), + 'calib.MCMC_params.kp_lognorm_mu': (int, float), + 'calib.MCMC_params.kp_lognorm_tau': (int, float), + 'calib.MCMC_params.kp_mu': (int, float), + 'calib.MCMC_params.kp_sigma': (int, float), + 'calib.MCMC_params.kp_bndlow': (int, float), + 'calib.MCMC_params.kp_bndhigh': (int, float), 'calib.MCMC_params.option_calib_elev_change_1d': bool, 'calib.MCMC_params.rhoabl_disttype': str, 'calib.MCMC_params.rhoabl_mu': (int, float), @@ -285,26 +285,26 @@ def _validate_config(self, config): 'sim.out.export_extra_vars': bool, 'sim.out.export_binned_data': bool, 'sim.out.export_binned_components': bool, - 'sim.out.export_binned_area_threshold': int, + 'sim.out.export_binned_area_threshold': (int, float), 'sim.oggm_dynamics': dict, 'sim.oggm_dynamics.cfl_number': float, 'sim.oggm_dynamics.cfl_number_calving': float, 'sim.oggm_dynamics.glen_a_regional_relpath': str, 'sim.oggm_dynamics.use_regional_glen_a': bool, - 'sim.oggm_dynamics.fs': int, - 'sim.oggm_dynamics.glen_a_multiplier': int, - 'sim.icethickness_advancethreshold': int, - 'sim.terminus_percentage': int, + 'sim.oggm_dynamics.fs': (int, float), + 'sim.oggm_dynamics.glen_a_multiplier': (int, float), + 'sim.icethickness_advancethreshold': (int, float), + 'sim.terminus_percentage': (int, float), 'sim.params': dict, 'sim.params.use_constant_lapserate': bool, - 'sim.params.kp': int, - 'sim.params.tbias': int, - 'sim.params.ddfsnow': float, - 'sim.params.ddfsnow_iceratio': float, - 'sim.params.precgrad': float, - 'sim.params.lapserate': float, - 'sim.params.tsnow_threshold': int, - 'sim.params.calving_k': float, + 'sim.params.kp': (int, float), + 'sim.params.tbias': (int, float), + 'sim.params.ddfsnow': (int, float), + 'sim.params.ddfsnow_iceratio': (int, float), + 'sim.params.precgrad': (int, float), + 'sim.params.lapserate': (int, float), + 'sim.params.tsnow_threshold': (int, float), + 'sim.params.calving_k': (int, float), 'mb': dict, 'mb.option_surfacetype_initial': int, 'mb.include_firn': bool, @@ -321,12 +321,12 @@ def _validate_config(self, config): 'mb.Woodard_rf_opts.rf_month': int, 'mb.HH2015_rf_opts': dict, 'mb.HH2015_rf_opts.rf_layers': int, - 'mb.HH2015_rf_opts.rf_dz': int, - 'mb.HH2015_rf_opts.rf_dsc': int, - 'mb.HH2015_rf_opts.rf_meltcrit': float, - 'mb.HH2015_rf_opts.pp': float, - 'mb.HH2015_rf_opts.rf_dens_top': int, - 'mb.HH2015_rf_opts.rf_dens_bot': int, + 'mb.HH2015_rf_opts.rf_dz': (int, float), + 'mb.HH2015_rf_opts.rf_dsc': (int, float), + 'mb.HH2015_rf_opts.rf_meltcrit': (int, float), + 'mb.HH2015_rf_opts.pp': (int, float), + 'mb.HH2015_rf_opts.rf_dens_top': (int, float), + 'mb.HH2015_rf_opts.rf_dens_bot': (int, float), 'mb.HH2015_rf_opts.option_rf_limit_meltsnow': int, 'rgi': dict, 'rgi.rgi_relpath': str, @@ -346,13 +346,13 @@ def _validate_config(self, config): 'time.summer_month_start': int, 'time.timestep': str, 'constants': dict, - 'constants.density_ice': int, - 'constants.density_water': int, - 'constants.k_ice': float, - 'constants.k_air': float, - 'constants.ch_ice': int, - 'constants.ch_air': int, - 'constants.Lh_rf': int, + 'constants.density_ice': (int, float), + 'constants.density_water': (int, float), + 'constants.k_ice': (int, float), + 'constants.k_air': (int, float), + 'constants.ch_ice': (int, float), + 'constants.ch_air': (int, float), + 'constants.Lh_rf': (int, float), 'constants.tolerance': float, 'debug': dict, 'debug.refreeze': bool, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 83bf12b8..5697e790 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -212,13 +212,13 @@ calib: h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ # 1d elevation change elev_change_1d: - elev_change_1d_relpath: /elev_change_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 1.00570_elev_change_1d_.json) + elev_change_1d_relpath: /elev_change_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 01.00570_elev_change_1d.json) # 1d melt extents meltextent_1d: - meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) + meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) # 1d snowlines snowline_1d: - snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) + snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) icethickness_cal_frac_byarea: 0.9 # Regional glacier area fraction that is used to calibrate the ice thickness # e.g., 0.9 means only the largest 90% of glaciers by area will be used to calibrate @@ -240,7 +240,7 @@ sim: - mad # export options (booleans) export_all_simiters: false # Exprort individual simulation results (false exports median and MAD from all sim_iters) - export_extra_vars: false # Option to export extra variables (temp, prec, melt, acc, etc.) + export_extra_vars: false # Option to export extra variables (temp, prec, melt, acc, etc.) export_binned_data: false # Export binned ice thickness export_binned_components: false # Export binned mass balance components (accumulation, melt, refreeze) export_binned_area_threshold: 0 # Area threshold for exporting binned ice thickness diff --git a/pygem/shop/mbdata.py b/pygem/shop/mbdata.py index 1ff56333..a507dfba 100755 --- a/pygem/shop/mbdata.py +++ b/pygem/shop/mbdata.py @@ -66,7 +66,6 @@ def mb_df_to_gdir( mbdata_fp = mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_fn'] assert os.path.exists(mbdata_fp), 'Error, mass balance dataset does not exist: {mbdata_fp}' - assert 'hugonnet2021' in mbdata_fp.lower(), 'Error, mass balance dataset not yet supported: {mbdata_fp}' rgiid_cn = 'rgiid' mb_cn = 'mb_mwea' mberr_cn = 'mb_mwea_err' diff --git a/pygem/tests/test_04_auxiliary.py b/pygem/tests/test_04_auxiliary.py new file mode 100644 index 00000000..c9db58fc --- /dev/null +++ b/pygem/tests/test_04_auxiliary.py @@ -0,0 +1,87 @@ +import glob +import os +import subprocess + +import pytest + +from pygem.setup.config import ConfigManager + +""" +Test suite to any necessary aux""" + + +@pytest.fixture(scope='module') +def rootdir(): + config_manager = ConfigManager() + pygem_prms = config_manager.read_config() + return pygem_prms['root'] + + +def test_simulation_massredistribution_dynamics(rootdir): + """ + Test the run_simulation CLI script with the "MassRedistributionCurves" dynamical option. + """ + + # Run run_simulation CLI script + subprocess.run( + [ + 'run_simulation', + '-rgi_glac_number', + '1.03622', + '-option_calibration', + 'MCMC', + '-sim_climate_name', + 'ERA5', + '-sim_startyear', + '2000', + '-sim_endyear', + '2019', + '-nsims', + '1', + '-option_dynamics', + 'MassRedistributionCurves', + '-outputfn_sfix', + 'mrcdynamics_', + ], + check=True, + ) + + # Check if output files were created + outdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'ERA5') + output_files = glob.glob(os.path.join(outdir, '**', '*_mrcdynamics_all.nc'), recursive=True) + assert output_files, f'Simulation output file not found in {outdir}' + + +def test_simulation_no_dynamics(rootdir): + """ + Test the run_simulation CLI script with no dynamics option. + """ + + # Run run_simulation CLI script + subprocess.run( + [ + 'run_simulation', + '-rgi_glac_number', + '1.03622', + '-option_calibration', + 'MCMC', + '-sim_climate_name', + 'ERA5', + '-sim_startyear', + '2000', + '-sim_endyear', + '2019', + '-nsims', + '1', + '-option_dynamics', + 'None', + '-outputfn_sfix', + 'nodynamics_', + ], + check=True, + ) + + # Check if output files were created + outdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'ERA5') + output_files = glob.glob(os.path.join(outdir, '**', '*_nodynamics_all.nc'), recursive=True) + assert output_files, f'Simulation output file not found in {outdir}' diff --git a/pygem/tests/test_04_postproc.py b/pygem/tests/test_05_postproc.py similarity index 78% rename from pygem/tests/test_04_postproc.py rename to pygem/tests/test_05_postproc.py index 3d50aa02..35a60eb8 100644 --- a/pygem/tests/test_04_postproc.py +++ b/pygem/tests/test_05_postproc.py @@ -16,14 +16,24 @@ def rootdir(): return pygem_prms['root'] -def test_postproc_monthly_mass(rootdir): +def test_postproc_subannual_mass(rootdir): """ - Test the postproc_monthly_mass CLI script. + Test the postproc_subannual_mass CLI script. """ simdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'CESM2', 'ssp245', 'stats') # Run postproc_monthyl_mass CLI script - subprocess.run(['postproc_monthly_mass', '-simdir', simdir], check=True) + subprocess.run(['postproc_subannual_mass', '-simdir', simdir], check=True) + + +def test_postproc_binned_subannual_thick(rootdir): + """ + Test the postproc_binned_subannual_thick CLI script. + """ + simdir = os.path.join(rootdir, 'Output', 'simulations', '01', 'CESM2', 'ssp245', 'binned') + + # Run postproc_monthyl_mass CLI script + subprocess.run(['postproc_binned_subannual_thick', '-simdir', simdir], check=True) def test_postproc_compile_simulations(rootdir): @@ -63,12 +73,12 @@ def test_check_compiled_product(rootdir): """ # skip variables that are not in the compiled products vars_to_skip = [ - 'glac_temp_monthly', + 'glac_temp', 'glac_mass_change_ignored_annual', - 'offglac_prec_monthly', - 'offglac_refreeze_monthly', - 'offglac_melt_monthly', - 'offglac_snowpack_monthly', + 'offglac_prec', + 'offglac_refreeze', + 'offglac_melt', + 'offglac_snowpack', ] simpath = os.path.join( @@ -79,7 +89,7 @@ def test_check_compiled_product(rootdir): 'CESM2', 'ssp245', 'stats', - '1.03622_CESM2_ssp245_MCMC_ba1_50sets_2000_2100_all.nc', + '1.03622_CESM2_ssp245_MCMC_ba1_10sets_2000_2100_all.nc', ) compdir = os.path.join(rootdir, 'Output', 'simulations', 'compile', 'glacier_stats') @@ -104,13 +114,13 @@ def test_check_compiled_product(rootdir): # pull data values simvals = simvar.values - compvals = compvar.values[0, :, :] # first index is the glacier index + compvals = compvar.values[0, :, :] # 0th index is the glacier index # check that compiled product has same shape as original data assert simvals.shape == compvals.shape, ( f'Compiled product shape {compvals.shape} does not match original data shape {simvals.shape}' ) # check that compiled product matches original data - assert np.all(np.array_equal(simvals, compvals)), ( + assert np.allclose(simvals, compvals, rtol=1e-8, atol=1e-12, equal_nan=True), ( f'Compiled product for {var} does not match original data' ) diff --git a/pygem/utils/_funcs.py b/pygem/utils/_funcs.py index 84ff15eb..ae57bf8a 100755 --- a/pygem/utils/_funcs.py +++ b/pygem/utils/_funcs.py @@ -52,14 +52,14 @@ def annualweightedmean_array(var, dates_table): var : np.ndarray Variable with monthly or daily timestep dates_table : pd.DataFrame - Table of dates, year, month, daysinmonth, wateryear, and season for each timestep + Table of dates, year, month, days_in_step, wateryear, and season for each timestep Returns ------- var_annual : np.ndarray Annual weighted mean of variable """ if pygem_prms['time']['timestep'] == 'monthly': - dayspermonth = dates_table['daysinmonth'].values.reshape(-1, 12) + dayspermonth = dates_table['days_in_step'].values.reshape(-1, 12) # creates matrix (rows-years, columns-months) of the number of days per month daysperyear = dayspermonth.sum(axis=1) # creates an array of the days per year (includes leap years) @@ -76,10 +76,10 @@ def annualweightedmean_array(var, dates_table): if var_annual.shape[1] == 1: var_annual = var_annual.reshape(var_annual.shape[0]) elif pygem_prms['time']['timestep'] == 'daily': - print( - '\nError: need to code the groupbyyearsum and groupbyyearmean for daily timestep.Exiting the model run.\n' - ) - exit() + var_annual = var.mean(1) + else: + # var_annual = var.mean(1) + assert 1 == 0, 'add this functionality for weighting that is not monthly or daily' return var_annual diff --git a/pyproject.toml b/pyproject.toml index 7ce3a44e..4d6b03ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,8 +50,8 @@ run_calibration_frontalablation = "pygem.bin.run.run_calibration_frontalablation run_calibration = "pygem.bin.run.run_calibration:main" run_mcmc_priors = "pygem.bin.run.run_mcmc_priors:main" run_simulation = "pygem.bin.run.run_simulation:main" -postproc_monthly_mass = "pygem.bin.postproc.postproc_monthly_mass:main" -postproc_binned_monthly_mass = "pygem.bin.postproc.postproc_binned_monthly_mass:main" +postproc_subannual_mass = "pygem.bin.postproc.postproc_subannual_mass:main" +postproc_binned_subannual_thick = "pygem.bin.postproc.postproc_binned_subannual_thick:main" postproc_distribute_ice = "pygem.bin.postproc.postproc_distribute_ice:main" postproc_compile_simulations = "pygem.bin.postproc.postproc_compile_simulations:main" list_failed_simulations = "pygem.bin.op.list_failed_simulations:main" From 476a2c78ba90aa06d981e923f3a5b10800c406b8 Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Mon, 3 Nov 2025 21:39:35 -0500 Subject: [PATCH 15/19] Check if relative paths exist (#144) Closes #138. --- .github/workflows/test_suite.yml | 4 +- config.yaml | 365 +++++++++++++++++++++++++++++++ pygem/bin/op/initialize.py | 4 +- pygem/setup/config.py | 62 +++++- pygem/tests/test_02_config.py | 4 +- 5 files changed, 430 insertions(+), 9 deletions(-) create mode 100644 config.yaml diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 2c30c1bb..8dd92a38 100644 --- a/.github/workflows/test_suite.yml +++ b/.github/workflows/test_suite.yml @@ -3,7 +3,7 @@ name: 'Install PyGEM and Run Test Suite' on: push: branches: - - master + - main - dev paths: - '**.py' @@ -58,7 +58,7 @@ jobs: - name: 'Clone the PyGEM-notebooks repo' run: | BRANCH=${GITHUB_REF#refs/heads/} - if [ "$BRANCH" = "master" ]; then + if [ "$BRANCH" = "main" ]; then NOTEBOOK_BRANCH="main" else NOTEBOOK_BRANCH="dev" diff --git a/config.yaml b/config.yaml new file mode 100644 index 00000000..5697e790 --- /dev/null +++ b/config.yaml @@ -0,0 +1,365 @@ +######################################################### +### Python Glacier Evolution Model configuration file ### +######################################################### +# some useful info: +# boolean options are either `true` or `false` +# exponential values are formatted as `##.#e+#` (mest be decimal point before `e`, with either `+` or `-` following) +# infintiy values are either `.inf` or `-.inf` +# `null` is imported to Python as `None` +# lists are indicated as: +# - item1 +# - item2 +# - item3 +######################################################### + +# ===== ROOT PATH ===== +root: /path/to/pygem/data/ # note, this parameter must be modfied as to point to the appropriate location. all other paths are assumed relative to this (inputs and outputs). + +# ===== USER INFO ===== +user: + name: David Rounce + institution: Carnegie Mellon University, Pittsburgh PA + email: drounce@cmu.edu + +# ===== GLACIER SELECTION ===== +setup: + rgi_region01: + - 1 + rgi_region02: all + glac_no_skip: null # optionally specify rgi glacier numbers to skip + glac_no: null # either `null` or a list of rgi glacier numbers, including the region # (e.g., 1.00570) + # - 1.00570 + # - 1.22193 + min_glac_area_km2: 0 # glacies below the specified area threshold will be excluded + # Types of glaciers to include (true) or exclude (false) + include_landterm: true # Switch to include land-terminating glaciers + include_laketerm: true # Switch to include lake-terminating glaciers + include_tidewater: true # Switch to include marine-terminating glaciers + include_frontalablation: true # Switch to ignore calving and treat tidewater glaciers as land-terminating + # note, calibration parameters from Rounc et al. (2023) already accounted for frontal ablation, + # and thus one cannot ignore frontal ablation if using them (must use parameters from calibration neglecting frontal ablation). + +# ===== OGGM SETTINGS ===== +oggm: + base_url: https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/L1-L2_files/elev_bands/ + logging_level: WORKFLOW # DEBUG, INFO, WARNING, ERROR, WORKFLOW, CRITICAL (recommended WORKFLOW) + border: 80 # 10, 80, 160, 240 (recommend 240 if expecting glaciers for long runs where glaciers may grow) + oggm_gdir_relpath: /oggm_gdirs/ + overwrite_gdirs: false + has_internet: true + +# ===== CLIMATE DATA AND TIME PERIODS ===== +climate: + # Reference period runs (reference period refers to the calibration period) + # This will typically vary between 1980-present + ref_climate_name: ERA5 # reference climate dataset + ref_startyear: 2000 # first year of model run (reference dataset) + ref_endyear: 2019 # last year of model run (reference dataset) + ref_wateryear: calendar # options for years: 'calendar', 'hydro', 'custom' + # GCM period used for simulation run + sim_climate_name: ERA5 # simulation climate dataset + sim_climate_scenario: null # simulation scenario + sim_startyear: 2000 # first year of model run (simulation dataset) + sim_endyear: 2019 # last year of model run (simulation dataset) + sim_wateryear: calendar # options for years: 'calendar', 'hydro', 'custom' + constantarea_years: 0 # number of years to not let the area or volume change + # ===== CLIMATE DATA FILEPATHS AND FILENAMES ===== + paths: + # ERA5 (default reference climate data) + era5_relpath: /climate_data/ERA5/ + era5_temp_fn: ERA5_temp_monthly.nc + era5_tempstd_fn: ERA5_tempstd_monthly.nc + era5_prec_fn: ERA5_totalprecip_monthly.nc + era5_elev_fn: ERA5_geopotential.nc + era5_pressureleveltemp_fn: ERA5_pressureleveltemp_monthly.nc + era5_lr_fn: ERA5_lapserates_monthly.nc + + # CMIP5 (GCM data) + cmip5_relpath: /climate_data/cmip5/ + cmip5_fp_var_ending: _r1i1p1_monNG/ + cmip5_fp_fx_ending: _r0i0p0_fx/ + # CMIP6 (GCM data) + cmip6_relpath: /climate_data/cmip6/ + # CESM2 Large Ensemble (GCM data) + cesm2_relpath: /climate_data/cesm2/ + cesm2_fp_var_ending: _mon/ + cesm2_fp_fx_ending: _fx/ + # GFDL SPEAR Large Ensemble (GCM data) + gfdl_relpath: /climate_data/gfdl/ + gfdl_fp_var_ending: _mon/ + gfdl_fp_fx_ending: _fx/ + +# ===== CALIBRATION OPTIONS ===== +calib: + option_calibration: MCMC # calibration option ('emulator', 'MCMC' 'HH2015', 'HH2015mod', 'null') + priors_reg_fn: priors_region.csv # Prior distribution (specify filename, relative to `path`/Output/calibration/, or set to null) + + # HH2015 params + HH2015_params: + tbias_init: 0 + tbias_step: 1 + kp_init: 1.5 + kp_bndlow: 0.8 + kp_bndhigh: 2 + ddfsnow_init: 0.003 + ddfsnow_bndlow: 0.00175 + ddfsnow_bndhigh: 0.0045 + + # HH2015mod params + HH2015mod_params: + tbias_init: 0 + tbias_step: 0.5 + kp_init: 1 + kp_bndlow: 0.5 + kp_bndhigh: 3 + ddfsnow_init: 0.0041 + method_opt: SLSQP # SciPy optimization scheme ('SLSQP' or 'L-BFGS-B') + params2opt: # parameters to optimize + - tbias + - kp + ftol_opt: 1e-3 # tolerance for SciPy optimization scheme + eps_opt: 0.01 # epsilon (adjust variables for jacobian) for SciPy optimization scheme (1e-6 works) + + # emulator params + emulator_params: + emulator_sims: 100 # Number of simulations to develop the emulator + overwrite_em_sims: true # Overwrite emulator simulations + opt_hh2015_mod: true # Option to also perform the HH2015_mod calibration using the emulator + tbias_step: 0.5 # tbias step size + tbias_init: 0 # tbias initial value + kp_init: 1 # kp initial value + kp_bndlow: 0.5 # kp lower bound + kp_bndhigh: 3 # kp upper bound + ddfsnow_init: 0.0041 # ddfsnow initial value + option_areaconstant: true # Option to keep area constant or evolve + tbias_disttype: truncnormal # Temperature bias distribution ('truncnormal', 'uniform') + tbias_sigma: 3 # tbias standard deviation for truncnormal distribution + kp_gamma_alpha: 2 # Precipitation factor gamma distribution alpha + kp_gamma_beta: 1 # Precipitation factor gamma distribution beta + ddfsnow_disttype: truncnormal # Degree-day factor of snow distribution ('truncnormal') + ddfsnow_mu: 0.0041 # ddfsnow mean + ddfsnow_sigma: 0.0015 # ddfsnow standard deviation + ddfsnow_bndlow: 0 # ddfsnow lower bound + ddfsnow_bndhigh: .inf # ddfsnow upper bound + method_opt: SLSQP # SciPy optimization scheme ('SLSQP' or 'L-BFGS-B') + params2opt: # parameters to optimize + - tbias + - kp + ftol_opt: 1e-6 # tolerance for SciPy optimization scheme + eps_opt: 0.01 # epsilon (adjust variables for jacobian) for SciPy optimization scheme + + # MCMC params + MCMC_params: + option_use_emulator: true # use emulator or full model (if true, calibration must have first been run with option_calibretion=='emulator') + emulator_sims: 100 + tbias_step: 0.1 + tbias_stepsmall: 0.05 + option_areaconstant: true # Option to keep area constant or evolve + # Chain options + mcmc_step: 0.5 # mcmc step size (in terms of standard deviation) + n_chains: 1 # number of chains (min 1, max 3) + mcmc_sample_no: 20000 # number of steps (10000 was found to be sufficient in HMA) + mcmc_burn_pct: 2 # percentage of steps to burn-in (0 records all steps in chain) + thin_interval: 10 # thin interval if need to reduce file size (best to leave at 1 if space allows) + # Degree-day factor of snow distribution options + ddfsnow_disttype: truncnormal # distribution type ('truncnormal', 'uniform') + ddfsnow_mu: 0.0041 # ddfsnow mean + ddfsnow_sigma: 0.0015 # ddfsnow standard deviation + ddfsnow_bndlow: 0 # ddfsnow lower bound + ddfsnow_bndhigh: .inf # ddfsnow upper bound + # Precipitation factor distribution options + kp_disttype: gamma # distribution type ('gamma' (recommended), 'lognormal', 'uniform') + # tbias and kp priors - won't be used if priors_reg_fn is null + tbias_disttype: normal # distribution type ('normal' (recommended), 'truncnormal', 'uniform') + tbias_mu: 0 # temperature bias mean of normal distribution + tbias_sigma: 1 # temperature bias mean of standard deviation + tbias_bndlow: -10 # temperature bias lower bound + tbias_bndhigh: 10 # temperature bias upper bound + kp_gamma_alpha: 9 # precipitation factor alpha value of gamma distribution + kp_gamma_beta: 4 # precipitation factor beta value of gamme distribution + kp_lognorm_mu: 0 # precipitation factor mean of log normal distribution + kp_lognorm_tau: 4 # precipitation factor tau of log normal distribution + kp_mu: 0 # precipitation factor mean of normal distribution + kp_sigma: 1.5 # precipitation factor standard deviation of normal distribution + kp_bndlow: 0.5 # precipitation factor lower bound + kp_bndhigh: 1.5 # precipitation factor upper bound + # options for calibrating against 1d elevation change + option_calib_elev_change_1d: false # option to calibrate against 1d elevation change data (true or false) + rhoabl_disttype: normal # ablation area density prior distribution type (currently only 'normal' option) + rhoabl_mu: 900 # ablation area density prior mean (kg m^-3) + rhoabl_sigma: 17 # ablation area density prior standard deviation (kg m^-3) + rhoaccum_disttype: normal # ablation area density prior distribution type (currently only 'normal' option) + rhoaccum_mu: 600 # accumulation area density prior mean (kg m^-3). default value from Huss, 2013 Table 1 + rhoaccum_sigma: 60 # accumulation area density prior standard deviation (kg m^-3). default value from Huss, 2013 Table 1 + # options for calibrating against 1d melt extent data + option_calib_meltextent_1d: false # option to calibrate against 1d melt extent data (true or false) + # options for calibrating against 1d snowline data + option_calib_snowline_1d: false # option to calibrate against 1d snowline data (true or false) + + # calibration datasets + data: + # mass balance data + massbalance: + hugonnet2021_relpath: /DEMs/Hugonnet2021/ # relative to main data path + hugonnet2021_fn: df_pergla_global_20yr-filled.csv # this file is 'raw', filled geodetic mass balance from Hugonnet et al. (2021) - pulled by prerproc_fetch_mbdata.py + hugonnet2021_facorrected_fn: df_pergla_global_20yr-filled-frontalablation-corrected.csv # frontal ablation corrected geodetic mass balance (produced by run_calibration_frontalablation.py) + # frontal ablation + frontalablation: + frontalablation_relpath: /frontalablation_data/ # relative to main data path + frontalablation_cal_fn: all-frontalablation_cal_ind.csv # merged frontalablation calibration data (produced by run_calibration_frontalablation.py) + # ice thickness + icethickness: + h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ + # 1d elevation change + elev_change_1d: + elev_change_1d_relpath: /elev_change_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 01.00570_elev_change_1d.json) + # 1d melt extents + meltextent_1d: + meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) + # 1d snowlines + snowline_1d: + snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) + + icethickness_cal_frac_byarea: 0.9 # Regional glacier area fraction that is used to calibrate the ice thickness + # e.g., 0.9 means only the largest 90% of glaciers by area will be used to calibrate + # glen's a for that region. + +# ===== SIMULATION ===== +sim: + option_dynamics: null # Glacier dynamics scheme (options: 'OGGM', 'MassRedistributionCurves', 'null') + option_bias_adjustment: 1 # Bias adjustment option (options: 0, 1, 2, 3) + # 0: no adjustment + # 1: new prec scheme and temp building on HH2015 + # 2: HH2015 methods + # 3: quantile delta mapping + nsims: 1 # number of simulations (note, defaults to 1 if ['calib']['option_calibration'] != 'MCMC') + # ===== OUTPUT OPTIONS ===== + out: + sim_stats: # Output statistics of simulations (options include any of the following 'mean', 'std', '2.5%', '25%', 'median', '75%', '97.5%', 'mad') + - median + - mad + # export options (booleans) + export_all_simiters: false # Exprort individual simulation results (false exports median and MAD from all sim_iters) + export_extra_vars: false # Option to export extra variables (temp, prec, melt, acc, etc.) + export_binned_data: false # Export binned ice thickness + export_binned_components: false # Export binned mass balance components (accumulation, melt, refreeze) + export_binned_area_threshold: 0 # Area threshold for exporting binned ice thickness + # ===== OGGM DYNAMICS ===== + oggm_dynamics: + cfl_number: 0.02 # Time step threshold (seconds) + cfl_number_calving: 0.01 # Time step threshold for marine-terimating glaciers (seconds) + use_regional_glen_a: true + glen_a_regional_relpath: /Output/calibration/glena_region.csv + # glen_a multiplier if not using regionally calibrated glens_a + fs: 0 + glen_a_multiplier: 1 + # Mass redistribution / Glacier geometry change options + icethickness_advancethreshold: 5 # advancing glacier ice thickness change threshold (5 m in Huss and Hock, 2015) + terminus_percentage: 20 # glacier (%) considered terminus (20% in HH2015), used to size advancing new bins + # ===== MODEL PARAMETERS ===== + params: + use_constant_lapserate: false # false: use spatially and temporally varying lapse rate, true: use constant value specified below + kp: 1 # precipitation factor [-] (referred to as k_p in Radic etal 2013; c_prec in HH2015) + tbias: 5 # temperature bias [deg C] + ddfsnow: 0.0041 # degree-day factor of snow [m w.e. d-1 degC-1] + ddfsnow_iceratio: 0.7 # Ratio degree-day factor snow snow to ice (Note, ddfice = ddfsnow / ddfsnow_iceratio) + precgrad: 0.0001 # precipitation gradient on glacier [m-1] + lapserate: -0.0065 # temperature lapse rate for both gcm to glacier and on glacier between elevation bins [K m-1] + tsnow_threshold: 1 # temperature threshold for snow [deg C] (HH2015 used 1.5 degC +/- 1 degC) + calving_k: 0.7 # frontal ablation rate [yr-1] + +# ===== MASS BALANCE MODEL OPTIONS ===== +mb: + # Initial surface type options + option_surfacetype_initial: 1 + # option 1 (default) - use median elevation to classify snow/firn above the median and ice below. + # > Sakai et al. (2015) found that the decadal ELAs are consistent with the median elevation of nine glaciers in High + # Mountain Asia, and Nuimura et al. (2015) also found that the snow line altitude of glaciers in China corresponded + # well with the median elevation. Therefore, the use of the median elevation for defining the initial surface type + # appears to be a fairly reasonable assumption in High Mountain Asia. + # option 2 - use mean elevation + include_firn: true # true: firn included, false: firn is modeled as snow + include_debris: true # true: account for debris with melt factors, false: do not account for debris + # debris datasets + debris_relpath: /debris_data/ + + # Downscaling model options + # Reference elevation options for downscaling climate variables + option_elev_ref_downscale: Zmed # 'Zmed', 'Zmax', or 'Zmin' for median, maximum or minimum glacier elevations + # Downscale temperature to bins options + option_adjusttemp_surfelev: 1 # 1: adjust temps based on surface elev changes; 0: no adjustment + # Downscale precipitation to bins options + option_preclimit: 0 # 1: limit the uppermost 25% using an expontial fxn + + # Accumulation model options + option_accumulation: 2 # 1: single threshold, 2: threshold +/- 1 deg using linear interpolation + + # Ablation model options + option_ablation: 1 # 1: monthly temp, 2: superimposed daily temps enabling melt near 0 (HH2015) + option_ddf_firn: 1 # 0: ddf_firn = ddf_snow; 1: ddf_firn = mean of ddf_snow and ddf_ice + + # Refreezing model option (options: 'Woodward' or 'HH2015') + # Woodward refers to Woodward et al. 1997 based on mean annual air temperature + # HH2015 refers to heat conduction in Huss and Hock 2015 + option_refreezing: Woodward # Woodward: annual air temp (Woodward etal 1997) + Woodard_rf_opts: + rf_month: 10 # refreeze month + HH2015_rf_opts: + rf_layers: 5 # number of layers for refreezing model (8 is sufficient - Matthias) + rf_dz: 2 # layer thickness (m) + rf_dsc: 3 # number of time steps for numerical stability (3 is sufficient - Matthias) + rf_meltcrit: 0.002 # critical amount of melt [m w.e.] for initializing refreezing module + pp: 0.3 # additional refreeze water to account for water refreezing at bare-ice surface + rf_dens_top: 300 # snow density at surface (kg m-3) + rf_dens_bot: 650 # snow density at bottom refreezing layer (kg m-3) + option_rf_limit_meltsnow: 1 + +# ===== RGI GLACIER DATA ===== +rgi: + # Filepath for RGI files + rgi_relpath: /RGI/rgi60/00_rgi60_attribs/ + # Column names + rgi_lat_colname: CenLat + rgi_lon_colname: CenLon_360 # REQUIRED OTHERWISE GLACIERS IN WESTERN HEMISPHERE USE 0 deg + elev_colname: elev + indexname: GlacNo + rgi_O1Id_colname: glacno + rgi_glacno_float_colname: RGIId_float + # Column names from table to drop (list names or accept an empty list) + rgi_cols_drop: + - GLIMSId + - BgnDate + - EndDate + - Status + - Linkages + - Name + +# ===== MODEL TIME PERIOD DETAILS ===== +time: + # Models require complete data for each year such that refreezing, scaling, etc. can be calculated + # Leap year option + option_leapyear: 0 # 1: include leap year days, 0: exclude leap years so February always has 28 days + # User specified start/end dates + # note: start and end dates must refer to whole years + startmonthday: 06-01 # Only used with custom calendars + endmonthday: 05-31 # Only used with custom calendars + wateryear_month_start: 10 # water year starting month + winter_month_start: 10 # first month of winter (for HMA winter is October 1 - April 30) + summer_month_start: 5 # first month of summer (for HMA summer is May 1 - Sept 30) + timestep: monthly # time step ('monthly' or 'daily') + +# ===== MODEL CONSTANTS ===== +constants: + density_ice: 900 # Density of ice [kg m-3] (or Gt / 1000 km3) + density_water: 1000 # Density of water [kg m-3] + k_ice: 2.33 # Thermal conductivity of ice [J s-1 K-1 m-1] recall (W = J s-1) + k_air: 0.023 # Thermal conductivity of air [J s-1 K-1 m-1] (Mellor, 1997) + ch_ice: 1890000 # Volumetric heat capacity of ice [J K-1 m-3] (density=900, heat_capacity=2100 J K-1 kg-1) + ch_air: 1297 # Volumetric Heat capacity of air [J K-1 m-3] (density=1.29, heat_capacity=1005 J K-1 kg-1) + Lh_rf: 333550 # Latent heat of fusion [J kg-1] + tolerance: 1.0e-12 # Model tolerance (used to remove low values caused by rounding errors) + +# ===== DEBUGGING OPTIONS ===== +debug: + refreeze: false + mb: false \ No newline at end of file diff --git a/pygem/bin/op/initialize.py b/pygem/bin/op/initialize.py index 0d0e38ca..7b14826e 100644 --- a/pygem/bin/op/initialize.py +++ b/pygem/bin/op/initialize.py @@ -16,10 +16,8 @@ from pygem.setup.config import ConfigManager -# instantiate ConfigManager +# instantiate ConfigManager - store new config.yaml file config_manager = ConfigManager(overwrite=True) -# read the config -pygem_prms = config_manager.read_config() def print_file_tree(start_path, indent=''): diff --git a/pygem/setup/config.py b/pygem/setup/config.py index b6bf47e4..efa73ac7 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -6,6 +6,7 @@ Distributed under the MIT license """ +import fnmatch import os import shutil @@ -17,7 +18,7 @@ class ConfigManager: """Manages PyGEMs configuration file, ensuring it exists, reading, updating, and validating its contents.""" - def __init__(self, config_filename='config.yaml', base_dir=None, overwrite=False): + def __init__(self, config_filename='config.yaml', base_dir=None, overwrite=False, check_paths=True): """ Initialize the ConfigManager class. @@ -25,12 +26,14 @@ def __init__(self, config_filename='config.yaml', base_dir=None, overwrite=False config_filename (str, optional): Name of the configuration file. Defaults to 'config.yaml'. base_dir (str, optional): Directory where the configuration file is stored. Defaults to '~/PyGEM'. overwrite (bool, optional): Whether to overwrite an existing configuration file. Defaults to False. + check_paths (bool, optional): Whether to check for relative paths existing. Defaults to True. """ self.config_filename = config_filename self.base_dir = base_dir or os.path.join(os.path.expanduser('~'), 'PyGEM') self.config_path = os.path.join(self.base_dir, self.config_filename) self.source_config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yaml') self.overwrite = overwrite + self.check_paths = check_paths self._ensure_config() def _ensure_config(self): @@ -97,6 +100,21 @@ def _validate_config(self, config): Parameters: config (dict): The configuration dictionary to be validated. """ + skip_patterns = [ + '*cesm2*', + '*cmip5*', + '*gfdl*', + '*debris*', + '*h_ref*', + '*frontalablation*', + '*snowline*', + '*meltextent*', + '*dh_1d*', + '*dhdt_2d*', + '*elev_change_1d*', + ] + + # --- Type validation (existing code) --- for key, expected_type in self.EXPECTED_TYPES.items(): keys = key.split('.') sub_data = config @@ -109,7 +127,6 @@ def _validate_config(self, config): if not isinstance(sub_data, expected_type): raise TypeError(f"Invalid type for '{key}': expected {expected_type}, not {type(sub_data)}") - # Check elements inside lists (if defined) if key in self.LIST_ELEMENT_TYPES and isinstance(sub_data, list): elem_type = self.LIST_ELEMENT_TYPES[key] if not all(isinstance(item, elem_type) for item in sub_data): @@ -117,7 +134,46 @@ def _validate_config(self, config): f"Invalid type for elements in '{key}': expected all elements to be {elem_type}, but got {sub_data}" ) - # check that all defined paths exist, raise error for any critical ones + if not self.check_paths: + return + + # --- Flatten the dict --- + def flatten_dict(d, parent_key=''): + items = {} + for k, v in d.items(): + new_key = f'{parent_key}.{k}' if parent_key else k + if isinstance(v, dict): + items.update(flatten_dict(v, new_key)) + else: + items[new_key] = v + return items + + flat = flatten_dict(config) + + # --- _relpath path validation --- + root = config.get('root') + if not root: + raise KeyError("Missing required 'root' key for path validation.") + root = os.path.abspath(root) + + for key, value in flat.items(): + if not key.endswith('_relpath') or not isinstance(value, str): + continue + + # Skip patterns + if any(fnmatch.fnmatch(key, pat) for pat in skip_patterns): + continue + print(key, value) + + path = os.path.join(root, value.strip(os.sep)) + + # Determine whether to check as file or directory + if os.path.splitext(path)[1]: # has an extension → treat as file + if not os.path.isfile(path): + raise FileNotFoundError(f"Missing file for '{key}': {path}") + else: # no extension → treat as directory + if not os.path.isdir(path): + raise FileNotFoundError(f"Missing directory for '{key}': {path}") # expected config types EXPECTED_TYPES = { diff --git a/pygem/tests/test_02_config.py b/pygem/tests/test_02_config.py index 243e0077..f6ba1f91 100644 --- a/pygem/tests/test_02_config.py +++ b/pygem/tests/test_02_config.py @@ -12,7 +12,9 @@ class TestConfigManager: @pytest.fixture(autouse=True) def setup(self, tmp_path): """Setup a ConfigManager instance for each test.""" - self.config_manager = ConfigManager(config_filename='config.yaml', base_dir=tmp_path, overwrite=True) + self.config_manager = ConfigManager( + config_filename='config.yaml', base_dir=tmp_path, overwrite=True, check_paths=False + ) def test_config_created(self, tmp_path): config_path = pathlib.Path(tmp_path) / 'config.yaml' From 4829129dc055ece90065e6253942048cc800753b Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Mon, 3 Nov 2025 21:40:44 -0500 Subject: [PATCH 16/19] Replace run_calibration_reg_glena.py with run_inversion.py (#149) Closes #148 --- docs/model_structure.md | 15 +- docs/run_calibration_reg_glena_overview.md | 24 - docs/run_inversion_overview.md | 22 + docs/scripts_overview.md | 2 +- pygem/bin/run/run_calibration_reg_glena.py | 563 --------------------- pygem/setup/config.py | 1 - pygem/setup/config.yaml | 4 - pyproject.toml | 1 - 8 files changed, 29 insertions(+), 603 deletions(-) delete mode 100644 docs/run_calibration_reg_glena_overview.md create mode 100644 docs/run_inversion_overview.md delete mode 100644 pygem/bin/run/run_calibration_reg_glena.py diff --git a/docs/model_structure.md b/docs/model_structure.md index 086f9326..01e241ff 100644 --- a/docs/model_structure.md +++ b/docs/model_structure.md @@ -32,9 +32,9 @@ If you use a different file structure and do not update the relative file paths 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: * [Pre-process data](preprocessing_target) (optional if including more data) * [Set up configuration file](config_workflow_target) -* [Calibrate frontal ablation parameter](workflow_cal_frontalablation_target) (optional for marine-terimating glaciers) * [Calibrate climatic mass balance parameters](workflow_cal_prms_target) -* [Calibrate ice viscosity parameter](workflow_cal_glena_target) +* [Calibrate frontal ablation parameter](workflow_cal_frontalablation_target) (optional for marine-terimating glaciers) +* [Calibrate ice viscosity parameter](workflow_run_inversion_target) * [Run model simulation](workflow_sim_target) * [Post-process output](workflow_post_target) * [Analyze output](workflow_analyze_target) @@ -92,17 +92,14 @@ Circularity issues exist in calibrating the frontal ablation parameter as the ma ``` -(workflow_cal_glena_target)= +(workflow_run_inversion_target)= ### Calibrate ice viscosity model parameter 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: ``` -run_calibration_reg_glena +run_inversion ``` -If successful, the script will run without error and output the following: -* ../Output/calibration/‘glena_region.csv’ - -For more details, see the [run_calibration_reg_glena.py Script Overview](run_calibration_reg_glena_overview_target). +For more details, see the [run_inversion.py Script Overview](run_inversion_overview_target). (workflow_sim_target)= @@ -130,7 +127,7 @@ For more details, see the [run_simulation.py Script Overview](run_simulation_tar 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: ``` -compile_simulations -rgi_region 01 -scenario ssp245 ssp370 ssp585 +postproc_compile_simulations -rgi_region 01 -scenario ssp245 ssp370 ssp585 ``` (workflow_analyze_target)= diff --git a/docs/run_calibration_reg_glena_overview.md b/docs/run_calibration_reg_glena_overview.md deleted file mode 100644 index 238db917..00000000 --- a/docs/run_calibration_reg_glena_overview.md +++ /dev/null @@ -1,24 +0,0 @@ -(run_calibration_reg_glena_overview_target)= -# run_calibration_reg_glena.py -This script will calibrate 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: - -``` -run_calibration_reg_glena -rgi_region01 -``` - -If successful, the script will run without error and output the following: -* ../Output/calibration/‘glena_region.csv’ - -## Script Structure - -Broadly speaking, the script follows: -* Load glaciers -* Select subset of glaciers to reduce computational expense -* Load climate data -* Run mass balance and invert for initial ice thickness -* 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 -* Export the calibrated parameters. - -## Special Considerations -* While the code runs at the RGI Order 1 region scale, it will only calibrate for the glaciers that have calibration data and run successfully. -* *~/PyGEM/config.yaml* has a parameter `calib.icethickness_cal_frac_byarea` that is used to set the fraction of glaciers by area to include in this calibration. The default is 0.9 (i.e., the largest 90% of glaciers by area). This is to reduce computational expense, since the smallest 10% of glaciers by area contribute very little to the regional volume. \ No newline at end of file diff --git a/docs/run_inversion_overview.md b/docs/run_inversion_overview.md new file mode 100644 index 00000000..8d2d3056 --- /dev/null +++ b/docs/run_inversion_overview.md @@ -0,0 +1,22 @@ +(run_inversion_overview_target)= +# run_inversion.py +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: + +``` +run_inversion -rgi_region01 +``` + +## Script Structure + +Broadly speaking, the script follows: +* Load glaciers +* Load climate data +* Compute apparent mass balance and invert for initial ice thickness +* 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 +* Export the calibrated parameters + +## Special Considerations +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.: +``` +{"dem_source": "COPDEM90", "flowline_type": "elevation_band", "apparent_mb_from_any_mb_residual": 2893.2237556771674, "inversion_glen_a": 3.784593106855888e-24, "inversion_fs": 0} +``` \ No newline at end of file diff --git a/docs/scripts_overview.md b/docs/scripts_overview.md index 06118aa3..99cd6229 100644 --- a/docs/scripts_overview.md +++ b/docs/scripts_overview.md @@ -9,7 +9,7 @@ maxdepth: 1 --- run_calibration_frontalablation_overview +run_inversion_overview run_calibration_overview -run_calibration_reg_glena_overview run_simulation_overview ``` \ No newline at end of file diff --git a/pygem/bin/run/run_calibration_reg_glena.py b/pygem/bin/run/run_calibration_reg_glena.py deleted file mode 100644 index d5503265..00000000 --- a/pygem/bin/run/run_calibration_reg_glena.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2018 David Rounce - -Distributed under the MIT license - -Find the optimal values of glens_a_multiplier to match the consensus ice thickness estimates -""" - -# Built-in libraries -import argparse -import json -import os -import pickle -import time -from collections import OrderedDict - -import matplotlib.pyplot as plt -import numpy as np - -# External libraries -import pandas as pd -from scipy.optimize import brentq - -# pygem imports -from pygem.setup.config import ConfigManager - -# instantiate ConfigManager -config_manager = ConfigManager() -# read the config -pygem_prms = config_manager.read_config() -# oggm imports -from oggm import cfg, tasks -from oggm.core.massbalance import apparent_mb_from_any_mb - -import pygem.pygem_modelsetup as modelsetup -from pygem import class_climate -from pygem.massbalance import PyGEMMassBalance -from pygem.oggm_compat import single_flowline_glacier_directory - - -# %% FUNCTIONS -def getparser(): - """ - Use argparse to add arguments from the command line - - Parameters - ---------- - ref_climate_name (optional) : str - reference gcm name - rgi_glac_number_fn : str - filename of .pkl file containing a list of glacier numbers which is used to run batches on the supercomputer - rgi_glac_number : str - rgi glacier number to run for supercomputer - option_ordered : bool (default: False) - option to keep glaciers ordered or to grab every n value for the batch - (the latter helps make sure run times on each core are similar as it removes any timing differences caused by - regional variations) - debug : bool (defauls: False) - Switch for turning debug printing on or off (default = 0 (off)) - - Returns - ------- - Object containing arguments and their respective values. - """ - parser = argparse.ArgumentParser(description='run calibration in parallel') - # add arguments - parser.add_argument( - '-rgi_region01', - type=int, - default=pygem_prms['setup']['rgi_region01'], - help='Randoph Glacier Inventory region (can take multiple, e.g. `-run_region01 1 2 3`)', - nargs='+', - ) - parser.add_argument( - '-ref_climate_name', - action='store', - type=str, - default=pygem_prms['climate']['ref_climate_name'], - help='reference gcm name', - ) - parser.add_argument( - '-ref_startyear', - action='store', - type=int, - default=pygem_prms['climate']['ref_startyear'], - help='reference period starting year for calibration (typically 2000)', - ) - parser.add_argument( - '-ref_endyear', - action='store', - type=int, - default=pygem_prms['climate']['ref_endyear'], - help='reference period ending year for calibration (typically 2019)', - ) - parser.add_argument( - '-rgi_glac_number_fn', - action='store', - type=str, - default=None, - help='Filename containing list of rgi_glac_number, helpful for running batches on spc', - ) - parser.add_argument( - '-rgi_glac_number', - action='store', - type=float, - default=pygem_prms['setup']['glac_no'], - nargs='+', - help='Randoph Glacier Inventory glacier number (can take multiple)', - ) - parser.add_argument( - '-fs', - action='store', - type=float, - default=pygem_prms['out']['fs'], - help='Sliding parameter', - ) - parser.add_argument( - '-a_multiplier', - action='store', - type=float, - default=pygem_prms['out']['glen_a_multiplier'], - help='Glen’s creep parameter A multiplier', - ) - parser.add_argument( - '-a_multiplier_bndlow', - action='store', - type=float, - default=0.1, - help='Glen’s creep parameter A multiplier, low bound (default 0.1)', - ) - parser.add_argument( - '-a_multiplier_bndhigh', - action='store', - type=float, - default=10, - help='Glen’s creep parameter A multiplier, upper bound (default 10)', - ) - - # flags - parser.add_argument( - '-option_ordered', - action='store_true', - help='Flag to keep glacier lists ordered (default is off)', - ) - parser.add_argument('-v', '--debug', action='store_true', help='Flag for debugging') - return parser - - -def plot_nfls_section(nfls): - """ - Modification of OGGM's plot_modeloutput_section() - """ - fig = plt.figure(figsize=(10, 6)) - ax = fig.add_axes([0.07, 0.08, 0.7, 0.84]) - - # Compute area histo - area = np.array([]) - height = np.array([]) - bed = np.array([]) - for cls in nfls: - a = cls.widths_m * cls.dx_meter * 1e-6 - a = np.where(cls.thick > 0, a, 0) - area = np.concatenate((area, a)) - height = np.concatenate((height, cls.surface_h)) - bed = np.concatenate((bed, cls.bed_h)) - ylim = [bed.min(), height.max()] - - # Plot histo - posax = ax.get_position() - posax = [ - posax.x0 + 2 * posax.width / 3.0, - posax.y0, - posax.width / 3.0, - posax.height, - ] - axh = fig.add_axes(posax, frameon=False) - - axh.hist(height, orientation='horizontal', range=ylim, bins=20, alpha=0.3, weights=area) - axh.invert_xaxis() - axh.xaxis.tick_top() - axh.set_xlabel('Area incl. tributaries (km$^2$)') - axh.xaxis.set_label_position('top') - axh.set_ylim(ylim) - axh.yaxis.set_ticks_position('right') - axh.set_yticks([]) - axh.axhline(y=ylim[1], color='black', alpha=1) # qick n dirty trick - - # plot Centerlines - cls = nfls[-1] - x = np.arange(cls.nx) * cls.dx * cls.map_dx - - # Plot the bed - ax.plot(x, cls.bed_h, color='k', linewidth=2.5, label='Bed (Parab.)') - - # Where trapezoid change color - if hasattr(cls, '_do_trapeze') and cls._do_trapeze: - bed_t = cls.bed_h * np.nan - pt = cls.is_trapezoid & (~cls.is_rectangular) - bed_t[pt] = cls.bed_h[pt] - ax.plot(x, bed_t, color='rebeccapurple', linewidth=2.5, label='Bed (Trap.)') - bed_t = cls.bed_h * np.nan - bed_t[cls.is_rectangular] = cls.bed_h[cls.is_rectangular] - ax.plot(x, bed_t, color='crimson', linewidth=2.5, label='Bed (Rect.)') - - # Plot glacier - def surf_to_nan(surf_h, thick): - t1 = thick[:-2] - t2 = thick[1:-1] - t3 = thick[2:] - pnan = ((t1 == 0) & (t2 == 0)) & ((t2 == 0) & (t3 == 0)) - surf_h[np.where(pnan)[0] + 1] = np.nan - return surf_h - - surfh = surf_to_nan(cls.surface_h, cls.thick) - ax.plot(x, surfh, color='#003399', linewidth=2, label='Glacier') - - ax.set_ylim(ylim) - - ax.spines['top'].set_color('none') - ax.xaxis.set_ticks_position('bottom') - ax.set_xlabel('Distance along flowline (m)') - ax.set_ylabel('Altitude (m)') - - # Legend - handles, labels = ax.get_legend_handles_labels() - by_label = OrderedDict(zip(labels, handles)) - ax.legend( - list(by_label.values()), - list(by_label.keys()), - bbox_to_anchor=(0.5, 1.0), - frameon=False, - ) - plt.show() - - -def reg_vol_comparison(gdirs, mbmods, a_multiplier=1, fs=0, debug=False): - """Calculate the modeled volume [km3] and consensus volume [km3] for the given set of glaciers""" - - reg_vol_km3_consensus = 0 - reg_vol_km3_modeled = 0 - for nglac, gdir in enumerate(gdirs): - if nglac % 2000 == 0: - print(gdir.rgi_id) - mbmod_inv = mbmods[nglac] - - # Arbitrariliy shift the MB profile up (or down) until mass balance is zero (equilibrium for inversion) - apparent_mb_from_any_mb(gdir, mb_model=mbmod_inv) - - tasks.prepare_for_inversion(gdir) - tasks.mass_conservation_inversion(gdir, glen_a=cfg.PARAMS['glen_a'] * a_multiplier, fs=fs) - tasks.init_present_time_glacier(gdir) # adds bins below - nfls = gdir.read_pickle('model_flowlines') - - # Load consensus volume - if os.path.exists(gdir.get_filepath('consensus_mass')): - consensus_fn = gdir.get_filepath('consensus_mass') - with open(consensus_fn, 'rb') as f: - consensus_km3 = pickle.load(f) / pygem_prms['constants']['density_ice'] / 1e9 - - reg_vol_km3_consensus += consensus_km3 - reg_vol_km3_modeled += nfls[0].volume_km3 - - if debug: - plot_nfls_section(nfls) - print('\n\n Modeled vol [km3]: ', nfls[0].volume_km3) - print(' Consensus vol [km3]:', consensus_km3, '\n\n') - - return reg_vol_km3_modeled, reg_vol_km3_consensus - - -# %% -def main(): - parser = getparser() - args = parser.parse_args() - time_start = time.time() - - if args.debug == 1: - debug = True - else: - debug = False - - # Calibrate each region - for reg in args.rgi_region01: - print('Region:', reg) - - # ===== LOAD GLACIERS ===== - main_glac_rgi_all = modelsetup.selectglaciersrgitable( - rgi_regionsO1=[reg], - rgi_regionsO2='all', - rgi_glac_number='all', - include_landterm=True, - include_laketerm=True, - include_tidewater=True, - ) - - main_glac_rgi_all = main_glac_rgi_all.sort_values('Area', ascending=False) - main_glac_rgi_all.reset_index(inplace=True, drop=True) - main_glac_rgi_all['Area_cum'] = np.cumsum(main_glac_rgi_all['Area']) - main_glac_rgi_all['Area_cum_frac'] = main_glac_rgi_all['Area_cum'] / main_glac_rgi_all.Area.sum() - - glac_idx = np.where(main_glac_rgi_all.Area_cum_frac > pygem_prms['calib']['icethickness_cal_frac_byarea'])[0][0] - main_glac_rgi_subset = main_glac_rgi_all.loc[0:glac_idx, :] - main_glac_rgi_subset = main_glac_rgi_subset.sort_values('O1Index', ascending=True) - main_glac_rgi_subset.reset_index(inplace=True, drop=True) - - print( - f'But only the largest {int(100 * pygem_prms["calib"]["icethickness_cal_frac_byarea"])}% of the glaciers by area, which includes', - main_glac_rgi_subset.shape[0], - 'glaciers.', - ) - - # ===== TIME PERIOD ===== - dates_table = modelsetup.datesmodelrun( - startyear=args.ref_startyear, - endyear=args.ref_endyear, - spinupyears=pygem_prms['climate']['ref_spinupyears'], - option_wateryear=pygem_prms['climate']['ref_wateryear'], - ) - - # ===== LOAD CLIMATE DATA ===== - # Climate class - sim_climate_name = args.ref_climate_name - assert sim_climate_name == 'ERA5', 'Error: Calibration not set up for ' + sim_climate_name - gcm = class_climate.GCM(name=sim_climate_name) - # Air temperature [degC] - gcm_temp, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.temp_fn, gcm.temp_vn, main_glac_rgi_subset, dates_table, verbose=debug - ) - if pygem_prms['mbmod']['option_ablation'] == 2 and sim_climate_name in ['ERA5']: - gcm_tempstd, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.tempstd_fn, - gcm.tempstd_vn, - main_glac_rgi_subset, - dates_table, - verbose=debug, - ) - else: - gcm_tempstd = np.zeros(gcm_temp.shape) - # Precipitation [m] - gcm_prec, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.prec_fn, gcm.prec_vn, main_glac_rgi_subset, dates_table, verbose=debug - ) - # Elevation [m asl] - gcm_elev = gcm.importGCMfxnearestneighbor_xarray(gcm.elev_fn, gcm.elev_vn, main_glac_rgi_subset) - # Lapse rate [degC m-1] - gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi_subset, dates_table, verbose=debug - ) - - # ===== RUN MASS BALANCE ===== - # Number of years (for OGGM's run_until_and_store) - if pygem_prms['time']['timestep'] == 'monthly': - nyears = int(dates_table.shape[0] / 12) - else: - assert True == False, 'Adjust nyears for non-monthly timestep' - - reg_vol_km3_consensus = 0 - reg_vol_km3_modeled = 0 - mbmods = [] - gdirs = [] - for glac in range(main_glac_rgi_subset.shape[0]): - # Select subsets of data - glacier_rgi_table = main_glac_rgi_subset.loc[main_glac_rgi_subset.index.values[glac], :] - glacier_str = '{0:0.5f}'.format(glacier_rgi_table['RGIId_float']) - - if glac % 1000 == 0: - print(glacier_str) - - # ===== Load glacier data: area (km2), ice thickness (m), width (km) ===== - try: - gdir = single_flowline_glacier_directory(glacier_str) - - # Flowlines - fls = gdir.read_pickle('inversion_flowlines') - - # Add climate data to glacier directory - gdir.historical_climate = { - 'elev': gcm_elev[glac], - 'temp': gcm_temp[glac, :], - 'tempstd': gcm_tempstd[glac, :], - 'prec': gcm_prec[glac, :], - 'lr': gcm_lr[glac, :], - } - gdir.dates_table = dates_table - - glacier_area_km2 = fls[0].widths_m * fls[0].dx_meter / 1e6 - if (fls is not None) and (glacier_area_km2.sum() > 0): - modelprms_fn = glacier_str + '-modelprms_dict.json' - modelprms_fp = ( - pygem_prms['root'] + '/Output/calibration/' + glacier_str.split('.')[0].zfill(2) + '/' - ) - modelprms_fullfn = modelprms_fp + modelprms_fn - assert os.path.exists(modelprms_fullfn), glacier_str + ' calibrated parameters do not exist.' - with open(modelprms_fullfn, 'r') as f: - modelprms_dict = json.load(f) - - assert 'emulator' in modelprms_dict, 'Error: ' + glacier_str + ' emulator not in modelprms_dict' - modelprms_all = modelprms_dict['emulator'] - - # Loop through model parameters - modelprms = { - 'kp': modelprms_all['kp'][0], - 'tbias': modelprms_all['tbias'][0], - 'ddfsnow': modelprms_all['ddfsnow'][0], - 'ddfice': modelprms_all['ddfice'][0], - 'tsnow_threshold': modelprms_all['tsnow_threshold'][0], - 'precgrad': modelprms_all['precgrad'][0], - } - - # ----- ICE THICKNESS INVERSION using OGGM ----- - # Apply inversion_filter on mass balance with debris to avoid negative flux - if pygem_prms['mbmod']['include_debris']: - inversion_filter = True - else: - inversion_filter = False - - # Perform inversion based on PyGEM MB - mbmod_inv = PyGEMMassBalance( - gdir, - modelprms, - glacier_rgi_table, - fls=fls, - option_areaconstant=True, - inversion_filter=inversion_filter, - ) - - # if debug: - # h, w = gdir.get_inversion_flowline_hw() - # mb_t0 = (mbmod_inv.get_annual_mb(h, year=0, fl_id=0, fls=fls) * cfg.SEC_IN_YEAR * - # pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water']) - # plt.plot(mb_t0, h, '.') - # plt.ylabel('Elevation') - # plt.xlabel('Mass balance (mwea)') - # plt.show() - - mbmods.append(mbmod_inv) - gdirs.append(gdir) - except: - print(glacier_str + ' failed - likely no gdir') - - print('\n\n------\nModel setup time:', time.time() - time_start, 's') - - # ===== CHECK BOUNDS ===== - reg_vol_km3_mod, reg_vol_km3_con = reg_vol_comparison( - gdirs, - mbmods, - a_multiplier=args.a_multiplier, - fs=args.fs, - debug=debug, - ) - # Lower bound - reg_vol_km3_mod_bndlow, reg_vol_km3_con = reg_vol_comparison( - gdirs, - mbmods, - a_multiplier=args.a_multiplier_bndlow, - fs=args.fs, - debug=debug, - ) - # Higher bound - reg_vol_km3_mod_bndhigh, reg_vol_km3_con = reg_vol_comparison( - gdirs, - mbmods, - a_multiplier=args.a_multiplier_bndhigh, - fs=args.fs, - debug=debug, - ) - - print('Region:', reg) - print('Consensus [km3] :', reg_vol_km3_con) - print('Model [km3] :', reg_vol_km3_mod) - print('Model bndlow [km3] :', reg_vol_km3_mod_bndlow) - print('Model bndhigh [km3]:', reg_vol_km3_mod_bndhigh) - - # ===== OPTIMIZATION ===== - # Check consensus is within bounds - if reg_vol_km3_con < reg_vol_km3_mod_bndhigh: - a_multiplier_opt = args.a_multiplier_bndhigh - elif reg_vol_km3_con > reg_vol_km3_mod_bndlow: - a_multiplier_opt = args.a_multiplier_bndhigh - # If so, then find optimal glens_a_multiplier - else: - - def to_minimize(a_multiplier): - """Objective function to minimize""" - reg_vol_km3_mod, reg_vol_km3_con = reg_vol_comparison( - gdirs, - mbmods, - a_multiplier=a_multiplier, - fs=args.fs, - debug=debug, - ) - return reg_vol_km3_mod - reg_vol_km3_con - - # Brentq minimization - a_multiplier_opt, r = brentq( - to_minimize, - args.a_multiplier_bndlow, - args.a_multiplier_bndhigh, - rtol=1e-2, - full_output=True, - ) - # Re-run to get estimates - reg_vol_km3_mod, reg_vol_km3_con = reg_vol_comparison( - gdirs, - mbmods, - a_multiplier=a_multiplier_opt, - fs=args.fs, - debug=debug, - ) - - print('\n\nOptimized:\n glens_a_multiplier:', np.round(a_multiplier_opt, 3)) - print(' Consensus [km3]:', reg_vol_km3_con) - print(' Model [km3] :', reg_vol_km3_mod) - - # ===== EXPORT RESULTS ===== - glena_cns = [ - 'O1Region', - 'count', - 'glens_a_multiplier', - 'fs', - 'reg_vol_km3_consensus', - 'reg_vol_km3_modeled', - ] - glena_df_single = pd.DataFrame(np.zeros((1, len(glena_cns))), columns=glena_cns) - glena_df_single.loc[0, :] = [ - reg, - main_glac_rgi_subset.shape[0], - a_multiplier_opt, - args.fs, - reg_vol_km3_con, - reg_vol_km3_mod, - ] - - try: - glena_df = pd.read_csv(f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}') - - # Add or overwrite existing file - glena_idx = np.where((glena_df.O1Region == reg))[0] - if len(glena_idx) > 0: - glena_df.loc[glena_idx, :] = glena_df_single.values - else: - glena_df = pd.concat([glena_df, glena_df_single], axis=0) - - except FileNotFoundError: - glena_df = glena_df_single - - except Exception as err: - print(f'Error saving results: {err}') - - glena_df = glena_df.sort_values('O1Region', ascending=True) - glena_df.reset_index(inplace=True, drop=True) - glena_df.to_csv( - f'{pygem_prms["root"]}/{pygem_prms["out"]["glen_a_regional_relpath"]}', - index=False, - ) - - print('\n\n------\nTotal processing time:', time.time() - time_start, 's') - - -if __name__ == '__main__': - main() diff --git a/pygem/setup/config.py b/pygem/setup/config.py index efa73ac7..c7fbff7f 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -330,7 +330,6 @@ def flatten_dict(d, parent_key=''): 'calib.data.meltextent_1d.meltextent_1d_relpath': (str, type(None)), 'calib.data.snowline_1d': dict, 'calib.data.snowline_1d.snowline_1d_relpath': (str, type(None)), - 'calib.icethickness_cal_frac_byarea': float, 'sim': dict, 'sim.option_dynamics': (str, type(None)), 'sim.option_bias_adjustment': int, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 5697e790..dc382971 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -220,10 +220,6 @@ calib: snowline_1d: snowline_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _snowline_elev.csv (e.g., 01.00570_snowline_elev.csv) - icethickness_cal_frac_byarea: 0.9 # Regional glacier area fraction that is used to calibrate the ice thickness - # e.g., 0.9 means only the largest 90% of glaciers by area will be used to calibrate - # glen's a for that region. - # ===== SIMULATION ===== sim: option_dynamics: null # Glacier dynamics scheme (options: 'OGGM', 'MassRedistributionCurves', 'null') diff --git a/pyproject.toml b/pyproject.toml index 4d6b03ba..dcb5a13c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ preproc_fetch_mbdata = "pygem.bin.preproc.preproc_fetch_mbdata:main" preproc_wgms_estimate_kp = "pygem.bin.preproc.preproc_wgms_estimate_kp:main" run_spinup = "pygem.bin.run.run_spinup:main" run_inversion = "pygem.bin.run.run_inversion:main" -run_calibration_reg_glena = "pygem.bin.run.run_calibration_reg_glena:main" run_calibration_frontalablation = "pygem.bin.run.run_calibration_frontalablation:main" run_calibration = "pygem.bin.run.run_calibration:main" run_mcmc_priors = "pygem.bin.run.run_mcmc_priors:main" From 7ad4cae005dd0a98c7243a470c4b8a4f719c5026 Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Tue, 4 Nov 2025 10:35:39 -0500 Subject: [PATCH 17/19] Reformat MCMC calibration framework to make more modular (#155) Closes #151 * set up obs and preds of mcmc as dictionary and match keys when calculating log likelihood of each step Closes #154 * Plot residual for any pred-obs pairs that aren't glacierwide mb, whether 2d or 1d --- pygem/bin/run/run_calibration.py | 252 ++++++++++++++++++------------- pygem/bin/run/run_inversion.py | 10 +- pygem/mcmc.py | 84 +++++------ pygem/plot/graphics.py | 113 ++++++++++---- pygem/setup/config.py | 2 +- pygem/setup/config.yaml | 1 + 6 files changed, 278 insertions(+), 184 deletions(-) diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index a25597e9..e35fe76e 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -113,14 +113,12 @@ def getparser(): default=pygem_prms['climate']['ref_endyear'], help='reference period ending year for calibration (typically 2019)', ) - ( - parser.add_argument( - '-rgi_glac_number_fn', - action='store', - type=str, - default=None, - help='filepath containing list of rgi_glac_number, helpful for running batches on spc', - ) + parser.add_argument( + '-rgi_glac_number_fn', + action='store', + type=str, + default=None, + help='filepath containing list of rgi_glac_number, helpful for running batches on spc', ) parser.add_argument( '-rgi_glac_number', @@ -179,6 +177,12 @@ def getparser(): action='store_true', help='Flag to keep glacier lists ordered (default is false)', ) + parser.add_argument( + '-option_calib_glacierwide_mb_mwea', + action='store_true', + default=pygem_prms['calib']['MCMC_params']['option_calib_glacierwide_mb_mwea'], + help='Flag to calibrate against average glacierwide mass balance', + ) parser.add_argument( '-option_calib_elev_change_1d', action='store_true', @@ -252,27 +256,20 @@ def mb_mwea_calc( return mb_mwea -def calculate_elev_change_1d( - gdir, - modelprms, - glacier_rgi_table, - fls, - debug=False, -): - """ - For a given set of model parameters, run the ice thickness inversion and mass balance model to get binned annual ice thickness change - Convert to monthly thickness by assuming that the flux divergence is constant throughout the year - """ +def run_oggm_dynamics(gdir, modelprms, glacier_rgi_table, fls): + """run the dynamical evolution model with a given set of model parameters""" + y0 = gdir.dates_table.year.min() y1 = gdir.dates_table.year.max() + # mass balance model with evolving area + mbmod = PyGEMMassBalance(gdir, modelprms, glacier_rgi_table, fls=fls) + # Check that water level is within given bounds cls = gdir.read_pickle('inversion_input')[-1] th = cls['hgt'][-1] vmin, vmax = cfg.PARAMS['free_board_marine_terminating'] water_level = utils.clip_scalar(0, th - vmax, th - vmin) - # mass balance model with evolving area - mbmod = PyGEMMassBalance(gdir, modelprms, glacier_rgi_table, fls=fls) # glacier dynamics model if gdir.is_tidewater and pygem_prms['setup']['include_frontalablation']: ev_model = flowline.FluxBasedModel( @@ -313,49 +310,55 @@ def calculate_elev_change_1d( for n in np.arange(calving_m3we_annual.shape[0]): ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3we_annual[n] - # Add mass lost from frontal ablation to Glacier-wide total mass balance (m3 w.e.) + # add mass lost from frontal ablation to Glacier-wide total mass balance (m3 w.e.) ev_model.mb_model.glac_wide_massbaltotal = ( ev_model.mb_model.glac_wide_massbaltotal + ev_model.mb_model.glac_wide_frontalablation ) + # safely catch any errors with dynamical run + except Exception: + ds = None - glacierwide_mb_mwea = ( - mbmod.glac_wide_massbaltotal[gdir.mbdata['t1_idx'] : gdir.mbdata['t2_idx'] + 1].sum() - / mbmod.glac_wide_area_annual[0] - / gdir.mbdata['nyears'] - ) + return mbmod, ds - # if there is an issue evaluating the dynamics model for a given parameter set in MCMC calibration, - # return -inf for mb_mwea and binned_dh, so MCMC calibration won't accept given parameters - except RuntimeError: - return float('-inf'), float('-inf') - ### get subannual elevation change - - # --- Step 1: convert mass balance from m w.e. to m ice --- - rho_w = pygem_prms['constants']['density_water'] - rho_i = pygem_prms['constants']['density_ice'] - bin_massbalclim_ice = mbmod.glac_bin_massbalclim * (rho_w / rho_i) # binned climatic mass balance (nbins, nsteps) +def calc_thick_change_1d(gdir, mbmod, ds): + """ + calculate binned change in ice thickness assuming constant annual flux divergence. + sub-annual ice thickness is differenced at timesteps coincident with observations. + """ + years_subannual = np.array([d.year for d in gdir.dates_table['date']]) + yrs = np.unique(years_subannual) + nyrs = len(yrs) + # grab components of interest + bin_thick_annual = ds[0].thickness_m.values.T # glacier thickness [m ice], (nbins, nyears) + + # set any < 0 thickness to nan + bin_thick_annual[bin_thick_annual <= 0] = np.nan + + # --- Step 1: convert mass balance from m w.e. to m ice --- + bin_massbalclim = mbmod.glac_bin_massbalclim # climatic mass balance [m w.e.] per step + # convert to m ice + bin_massbalclim_ice = bin_massbalclim * ( + pygem_prms['constants']['density_water'] / pygem_prms['constants']['density_ice'] + ) # --- Step 2: expand flux divergence to subannual steps --- - # assume the flux divergence is constant througohut the year - # ie. take annual values and divide spread uniformly throughout model year + # assume flux divergence is constant throughout the year + # (divide annual by the number of steps in the binned climatic mass balance to get subannual flux divergence) + bin_flux_divergence_annual = -ds[0].flux_divergence_myr.values.T[:, 1:] bin_flux_divergence_subannual = np.zeros_like(bin_massbalclim_ice) - for i, year in enumerate(gdir.dates_table.year.unique()): - idx = np.where(gdir.dates_table.year.values == year)[0] - bin_flux_divergence_subannual[:, idx] = -ds[0].flux_divergence_myr.values.T[:, i + 1][:, np.newaxis] / len( - idx - ) # note, oggm flux_divergence_myr is opposite sign of convention, hence negative + for i, year in enumerate(yrs): + idx = np.where(years_subannual == year)[0] + bin_flux_divergence_subannual[:, idx] = bin_flux_divergence_annual[:, i][:, np.newaxis] / len(idx) # --- Step 3: compute subannual thickness change --- - bin_delta_thick_subannual = bin_massbalclim_ice - bin_flux_divergence_subannual # [m ice] + bin_delta_thick_subannual = bin_massbalclim_ice - bin_flux_divergence_subannual - # --- Step 4: calculate subannual thickness --- - # calculate binned subannual thickness = running thickness change + initial thickness - bin_thick_initial = ds[0].thickness_m.isel(time=0).values # initial glacier thickness [m ice], (nbins) + # --- Step 4: calculate subannual thickness = running thickness change + initial thickness--- running_bin_delta_thick_subannual = np.cumsum(bin_delta_thick_subannual, axis=-1) - bin_thick_subannual = running_bin_delta_thick_subannual + bin_thick_initial[:, np.newaxis] + bin_thick_subannual = running_bin_delta_thick_subannual + bin_thick_annual[:, 0][:, np.newaxis] - # --- Step 5: rebin subannual thickness --- + # --- Step 5: rebin --- # get surface height at the specified reference year ref_surface_height = ds[0].bed_h.values + ds[0].thickness_m.sel(time=gdir.elev_change_1d['ref_dem_year']).values # aggregate model bin thicknesses as desired @@ -375,8 +378,8 @@ def calculate_elev_change_1d( # interpolate over any empty bins bin_thick_subannual = np.column_stack([interp1d_fill_gaps(x.copy()) for x in bin_thick_subannual.T]) - # --- Step 6: calculate elevation change --- - elev_change_1d = np.column_stack( + # --- Step 5: compute binned thickness change --- + bin_thick_change = np.column_stack( [ bin_thick_subannual[:, tup[1]] - bin_thick_subannual[:, tup[0]] if tup[0] is not None and tup[1] is not None @@ -385,7 +388,62 @@ def calculate_elev_change_1d( ] ) - return glacierwide_mb_mwea, elev_change_1d + return bin_thick_change + + +def mcmc_model_eval( + gdir, + modelprms, + glacier_rgi_table, + fls, + mbfxn=None, + calib_elev_change_1d=False, + calib_snowlines_1d=False, + calib_meltextent_1d=False, + debug=False, +): + """ + For a given set of model parameters, evaluate the desired model outputs. + Optionally use an emulator function to compute mass balance. + Returns a dictionary with only the requested results. + """ + results = {} + mbmod = None + + if calib_elev_change_1d: + mbmod, ds = run_oggm_dynamics(gdir, modelprms, glacier_rgi_table, fls) + # note, the binned thickness change is scaled by modeled density in mcmc.mbPosterior.log_likelihood() to calculate modeled surface elevation change + results['elev_change_1d'] = calc_thick_change_1d(gdir, mbmod, ds) if ds else float('-inf') + + if mbfxn is not None: + # grab current values from modelprms for the emulator + mb_args = [modelprms['tbias'], modelprms['kp'], modelprms['ddfsnow']] + glacierwide_mb_mwea = mbfxn(*[mb_args]) + else: + if mbmod is None: + glacierwide_mb_mwea = mb_mwea_calc(gdir, modelprms, glacier_rgi_table, fls) + else: + glacierwide_mb_mwea = ( + mbmod.glac_wide_massbaltotal[gdir.mbdata['t1_idx'] : gdir.mbdata['t2_idx'] + 1].sum() + / mbmod.glac_wide_area_annual[0] + / gdir.mbdata['nyears'] + ) + + results['glacierwide_mb_mwea'] = glacierwide_mb_mwea + + # (add future calibration options here) + if calib_snowlines_1d: + pass + # results["snowlines_1d"] = calc_snowlines_1d(gdir, mbmod) + + if calib_meltextent_1d: + pass + # results["meltextent_1d"] = calc_meltextent_1d(gdir, mbmod) + + if debug: + print('Returned keys:', list(results.keys())) + + return results # class for Gaussian Process model for mass balance emulator @@ -2012,8 +2070,14 @@ def rho_constraints(**kwargs): # ------------------- # --- set up MCMC --- # ------------------- - # mass balance observation and standard deviation - obs = [(torch.tensor([mb_obs_mwea]), torch.tensor([mb_obs_mwea_err]))] + # the mcmc.mbPosterior class expects observations to be provided as a dictionary, + # where each key corresponds to a variable being calibrated. + # each value should be a tuple of the form (observation, variance). + obs = {'glacierwide_mb_mwea': (torch.tensor([mb_obs_mwea]), torch.tensor([mb_obs_mwea_err]))} + mbfxn = None + + if pygem_prms['calib']['MCMC_params']['option_use_emulator']: + mbfxn = mbEmulator.eval # returns (mb_mwea) # if running full model (no emulator), or calibrating against binned elevation change, several arguments are needed if args.option_calib_elev_change_1d: @@ -2038,34 +2102,22 @@ def rho_constraints(**kwargs): ), ) - # calculate inds of data v. model - mbfxn = calculate_elev_change_1d # returns (mb_mwea, binned_dm) - mbargs = ( - gdir, # arguments for get_binned_dh() - modelprms, - glacier_rgi_table, - fls, + # add elevation change data to observations dictionary + obs['elev_change_1d'] = ( + torch.tensor(gdir.elev_change_1d['dh']), + torch.tensor(gdir.elev_change_1d['dh_sigma']), ) - # append deltah obs and and sigma obs list - obs.append( - ( - torch.tensor(gdir.elev_change_1d['dh']), - torch.tensor(gdir.elev_change_1d['dh_sigma']), - ) - ) - # if there are more observations to calibrate against, simply append a tuple of (obs, variance) to obs list - # e.g. obs.append((torch.tensor(dmda_array),torch.tensor(dmda_err_array))) - elif pygem_prms['calib']['MCMC_params']['option_use_emulator']: - mbfxn = mbEmulator.eval # returns (mb_mwea) - mbargs = None # no additional arguments for mbEmulator.eval() - else: - mbfxn = mb_mwea_calc # returns (mb_mwea) - mbargs = ( - gdir, - modelprms, - glacier_rgi_table, - fls, - ) # arguments for mb_mwea_calc() + # if there are more observations to calibrate against, simply add them as a tuple of (obs, variance) to the obs dictionary + + # define args to pass to fxn2eval in mcmc sampler + fxnargs = ( + gdir, + modelprms, + glacier_rgi_table, + fls, + mbfxn, + args.option_calib_elev_change_1d, + ) # instantiate mbPosterior given priors, and observed values # note, mbEmulator.eval expects the modelprms to be ordered like so: [tbias, kp, ddfsnow], so priors and initial guesses must also be ordered as such) @@ -2073,8 +2125,9 @@ def rho_constraints(**kwargs): mb = mcmc.mbPosterior( obs, priors, - mb_func=mbfxn, - mb_args=mbargs, + fxn2eval=mcmc_model_eval, + fxnargs=fxnargs, + calib_glacierwide_mb_mwea=args.option_calib_glacierwide_mb_mwea, potential_fxns=[mb_max, must_melt, rho_constraints], ela=gdir.ela.min() if hasattr(gdir, 'ela') else None, bin_z=gdir.elev_change_1d['bin_centers'] if hasattr(gdir, 'elev_change_1d') else None, @@ -2091,7 +2144,7 @@ def rho_constraints(**kwargs): if args.option_calib_elev_change_1d: modelprms_export['elev_change_1d'] = {} modelprms_export['elev_change_1d']['bin_edges'] = gdir.elev_change_1d['bin_edges'] - modelprms_export['elev_change_1d']['obs'] = [ob.flatten().tolist() for ob in obs[1]] + modelprms_export['elev_change_1d']['obs'] = [ob.flatten().tolist() for ob in obs['elev_change_1d']] modelprms_export['elev_change_1d']['dates'] = [ (dt1, dt2) for dt1, dt2 in gdir.elev_change_1d['dates'] ] @@ -2179,19 +2232,12 @@ def rho_constraints(**kwargs): f'Chain {n_chain}: failed to produce an unstuck result after {attempts_per_chain} initial guesses.' ) - # concatenate mass balance - m_chain = torch.cat((m_chain, torch.tensor(pred_chain[0]).reshape(-1, 1)), dim=1) - m_primes = torch.cat( - (m_primes, torch.tensor(pred_primes[0]).reshape(-1, 1)), - dim=1, - ) - if debug: print( 'mb_mwea_mean:', - np.round(torch.mean(m_chain[:, -1]).item(), 3), + np.round(torch.mean(torch.stack(pred_chain['glacierwide_mb_mwea'])).item(), 3), 'mb_mwea_std:', - np.round(torch.std(m_chain[:, -1]).item(), 3), + np.round(torch.std(torch.stack(pred_chain['glacierwide_mb_mwea'])).item(), 3), '\nmb_obs_mean:', np.round(mb_obs_mwea, 3), 'mb_obs_std:', @@ -2215,23 +2261,25 @@ def rho_constraints(**kwargs): graphics.plot_mcmc_chain( m_primes, m_chain, - obs[0], + pred_primes, + pred_chain, + obs, ar, glacier_str, show=show, fpath=f'{fp}/{glacier_str}-chain{n_chain}.png', ) - for i in pred_chain.keys(): + for k in pred_chain.keys(): graphics.plot_resid_histogram( - obs[i], - pred_chain[i], + obs[k], + pred_chain[k], glacier_str, show=show, - fpath=f'{fp}/{glacier_str}-chain{n_chain}-residuals-{i}.png', + fpath=f'{fp}/{glacier_str}-chain{n_chain}-residuals-{k}.png', ) - if i == 1: + if k == 'elev_change_1d': graphics.plot_mcmc_elev_change_1d( - pred_chain[1], + pred_chain[k], fls, gdir.elev_change_1d, gdir.ela.min(), @@ -2255,7 +2303,7 @@ def rho_constraints(**kwargs): modelprms_export['ar'][chain_str] = ar if args.option_calib_elev_change_1d: modelprms_export['elev_change_1d'][chain_str] = [ - preds.flatten().tolist() for preds in pred_chain[1] + preds.flatten().tolist() for preds in pred_chain['elev_change_1d'] ] modelprms_export['rhoabl'][chain_str] = m_chain[:, 3].tolist() modelprms_export['rhoacc'][chain_str] = m_chain[:, 4].tolist() diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index ab8a029c..24ffaa2b 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -55,15 +55,15 @@ def export_regional_results(regions, outpath): # sort by the region number merged_df = merged_df.sort_values('rnum').drop(columns='rnum') - # if the file already exists, replace rows with same '01Region' + # if the file already exists, replace rows with same 'O1Region' if os.path.exists(outpath): existing_df = pd.read_csv(outpath) - # remove rows with the same '01Region' values as in the new merge + # remove rows with the same 'O1Region' values as in the new merge merged_df = pd.concat( - [existing_df[~existing_df['01Region'].isin(merged_df['01Region'])], merged_df], ignore_index=True + [existing_df[~existing_df['O1Region'].isin(merged_df['O1Region'])], merged_df], ignore_index=True ) # re-sort - merged_df = merged_df.sort_values('01Region') + merged_df = merged_df.sort_values('O1Region') # export final merged csv merged_df.to_csv(outpath, index=False) @@ -350,7 +350,7 @@ def run( # prepare ouptut dataset df = pd.Series( { - '01Region': reg, + 'O1Region': reg, 'count': len(glac_no), 'inversion_glen_a': gdirs[0].get_diagnostics()['inversion_glen_a'], 'inversion_fs': gdirs[0].get_diagnostics()['inversion_fs'], diff --git a/pygem/mcmc.py b/pygem/mcmc.py index f149b9a2..b10ad61c 100644 --- a/pygem/mcmc.py +++ b/pygem/mcmc.py @@ -150,12 +150,15 @@ def log_uniform_density(x, **kwargs): # mass balance posterior class class mbPosterior: - def __init__(self, obs, priors, mb_func, mb_args=None, potential_fxns=None, **kwargs): + def __init__( + self, obs, priors, fxn2eval, fxnargs=None, calib_glacierwide_mb_mwea=True, potential_fxns=None, **kwargs + ): # obs will be passed as a list, where each item is a tuple with the first element being the mean observation, and the second being the variance self.obs = obs self.priors = copy.deepcopy(priors) - self.mb_func = mb_func - self.mb_args = mb_args + self.fxn2eval = fxn2eval + self.fxnargs = fxnargs + self.calib_glacierwide_mb_mwea = calib_glacierwide_mb_mwea self.potential_functions = potential_fxns if potential_fxns is not None else [] self.preds = None self.check_priors() @@ -197,19 +200,15 @@ def check_priors(self): # update modelprms for evaluation def update_modelprms(self, m): for i, k in enumerate(['tbias', 'kp', 'ddfsnow']): - self.mb_args[1][k] = float(m[i]) - self.mb_args[1]['ddfice'] = self.mb_args[1]['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] + self.fxnargs[1][k] = float(m[i]) + self.fxnargs[1]['ddfice'] = self.fxnargs[1]['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] - # get mb_pred + # get model predictions def get_model_pred(self, m): - if self.mb_args: - self.update_modelprms(m) - self.preds = self.mb_func(*self.mb_args) - else: - self.preds = self.mb_func([*m]) - if not isinstance(self.preds, tuple): - self.preds = [self.preds] - self.preds = [torch.tensor(item) for item in self.preds] # make all preds torch.tensor() objects + self.update_modelprms(m) # update modelprms with current step + self.preds = self.fxn2eval(*self.fxnargs) + # convert all values to torch tensors + self.preds = {k: torch.tensor(v, dtype=torch.float) for k, v in self.preds.items()} # get total log prior density def log_prior(self, m): @@ -225,36 +224,33 @@ def log_prior(self, m): # get log likelihood def log_likelihood(self, m): log_likehood = 0 - for i, pred in enumerate(self.preds): - # --- Check for invalid predictions early --- + for k, pred in self.preds.items(): + # --- Check for invalid predictions --- if torch.all(pred == float('-inf')): # Invalid model output -> assign -inf likelihood return torch.tensor([-float('inf')]) - if i == 0: - # --- Base case: mass balance likelihood --- - log_likehood += log_normal_density( - self.obs[i][0], # observed values - mu=pred, # predicted values - sigma=self.obs[i][1], # observation uncertainty - ) + if k == 'glacierwide_mb_mwea' and not self.calib_glacierwide_mb_mwea: + continue # skip this model output if not calibrating glacierwide mass balance - elif i == 1 and len(m) > 3: - # --- Extended case: apply density scaling to get binned elevation change --- + # if key is `elev_change_1d` scale by density to predict binned surface elevation change + if k == 'elev_change_1d': # Create density field, separate values for ablation/accumulation zones rho = np.ones_like(self.bin_z) rho[self.abl_mask] = m[3] # rhoabl rho[~self.abl_mask] = m[4] # rhoacc rho = torch.tensor(rho) - self.preds[i] = pred = ( - self.preds[i] * (pygem_prms['constants']['density_ice'] / rho[:, np.newaxis]) - ) # scale prediction by model density values (convert from m ice to m thickness change considering modeled density) - - log_likehood += log_normal_density( - self.obs[i][0], # observations - mu=pred, # scaled predictions - sigma=self.obs[i][1], # uncertainty - ) + # scale prediction by model density values (convert from m ice to m surface elevation change considering modeled density) + pred *= pygem_prms['constants']['density_ice'] / rho[:, np.newaxis] + # update values in preds dict + self.preds[k] = pred + + log_likehood += log_normal_density( + self.obs[k][0], # observations + mu=pred, # scaled predictions + sigma=self.obs[k][1], # uncertainty + ) + return log_likehood # compute the log-potential, summing over all declared potential functions. @@ -265,7 +261,7 @@ def log_potential(self, m): 'kp': m[0], 'tbias': m[1], 'ddfsnow': m[2], - 'massbal': self.preds[0], + 'massbal': self.preds['glacierwide_mb_mwea'], } # --- Optional arguments(if len(m) > 3) --- @@ -332,9 +328,9 @@ def rm_stuck_samples(self): self.m_primes = self.m_primes[self.n_rm :] self.steps = self.steps[self.n_rm :] self.acceptance = self.acceptance[self.n_rm :] - for j in self.preds_primes.keys(): - self.preds_primes[j] = self.preds_primes[j][self.n_rm :] - self.preds_chain[j] = self.preds_chain[j][self.n_rm :] + for k in self.preds_primes.keys(): + self.preds_primes[k] = self.preds_primes[k][self.n_rm :] + self.preds_chain[k] = self.preds_chain[k][self.n_rm :] return def sample( @@ -394,12 +390,12 @@ def sample( self.m_chain.append(m_0) self.m_primes.append(m_prime) self.acceptance.append(self.naccept / (i + (thin_factor * self.n_rm))) - for j in range(len(pred_1)): - if j not in self.preds_chain.keys(): - self.preds_chain[j] = [] - self.preds_primes[j] = [] - self.preds_chain[j].append(pred_0[j]) - self.preds_primes[j].append(pred_1[j]) + for k, values in pred_1.items(): + if k not in self.preds_chain.keys(): + self.preds_chain[k] = [] + self.preds_primes[k] = [] + self.preds_chain[k].append(pred_0[k]) + self.preds_primes[k].append(pred_1[k]) # trim off any initial steps that are stagnant if (i == (n_samples - 1)) and (trim): diff --git a/pygem/plot/graphics.py b/pygem/plot/graphics.py index 103725b3..9bd168d6 100644 --- a/pygem/plot/graphics.py +++ b/pygem/plot/graphics.py @@ -13,6 +13,7 @@ import matplotlib.pyplot as plt import numpy as np +import torch from scipy.stats import binned_statistic from pygem.utils.stats import effective_n @@ -119,10 +120,15 @@ def plot_modeloutput_section( ax.set_title(title, loc='left') -def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show=False, fpath=None): +def plot_mcmc_chain( + m_primes, m_chain, pred_primes, pred_chain, obs, ar, title, ms=1, fontsize=8, show=False, fpath=None +): # Plot the trace of the parameters - n = m_primes.shape[1] - fig, axes = plt.subplots(n + 1, 1, figsize=(6, n * 1.5), sharex=True) + nparams = m_primes.shape[1] + npreds = len(pred_chain.keys()) + N = nparams + npreds + 1 + fig, axes = plt.subplots(N, 1, figsize=(6, N * 1), sharex=True) + # convert torch objects to numpy m_chain = m_chain.detach().numpy() m_primes = m_primes.detach().numpy() @@ -131,6 +137,7 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show # instantiate list to hold legend objs legs = [] + # axes[0] will always be tbias axes[0].plot( [], [], @@ -145,6 +152,7 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show # axes[0].add_artist(leg) axes[0].set_ylabel(r'$T_{bias}$', fontsize=fontsize) + # axes[1] will always be kp axes[1].plot(m_primes[:, 1], '.', ms=ms, c='tab:blue') axes[1].plot(m_chain[:, 1], '.', ms=ms, c='tab:orange') axes[1].plot( @@ -156,6 +164,7 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show legs.append(l1) axes[1].set_ylabel(r'$K_p$', fontsize=fontsize) + # axes[2] will always be ddfsnow axes[2].plot(m_primes[:, 2], '.', ms=ms, c='tab:blue') axes[2].plot(m_chain[:, 2], '.', ms=ms, c='tab:orange') axes[2].plot( @@ -167,7 +176,8 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show legs.append(l2) axes[2].set_ylabel(r'$fsnow$', fontsize=fontsize) - if n > 4: + if nparams > 3: + # axes[3] will be rho_ablation if more than 3 model params m_chain[:, 3] = m_chain[:, 3] m_primes[:, 3] = m_primes[:, 3] axes[3].plot(m_primes[:, 3], '.', ms=ms, c='tab:blue') @@ -181,6 +191,7 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show legs.append(l3) axes[3].set_ylabel(r'$\rho_{abl}$', fontsize=fontsize) + # axes[4] will be rho_accumulation if more than 3 model params m_chain[:, 4] = m_chain[:, 4] m_primes[:, 4] = m_primes[:, 4] axes[4].plot(m_primes[:, 4], '.', ms=ms, c='tab:blue') @@ -194,31 +205,69 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show legs.append(l4) axes[4].set_ylabel(r'$\rho_{acc}$', fontsize=fontsize) - axes[-2].fill_between( - np.arange(len(ar)), - mb_obs[0] - (2 * mb_obs[1]), - mb_obs[0] + (2 * mb_obs[1]), - color='grey', - alpha=0.3, - ) - axes[-2].fill_between( - np.arange(len(ar)), - mb_obs[0] - mb_obs[1], - mb_obs[0] + mb_obs[1], - color='grey', - alpha=0.3, - ) - axes[-2].plot(m_primes[:, -1], '.', ms=ms, c='tab:blue') - axes[-2].plot(m_chain[:, -1], '.', ms=ms, c='tab:orange') - axes[-2].plot( - [], - [], - label=f'median={np.median(m_chain[:, -1]):.3f}\niqr={np.subtract(*np.percentile(m_chain[:, -1], [75, 25])):.3f}', - ) - ln2 = axes[-2].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) - legs.append(ln2) - axes[-2].set_ylabel(r'$\dot{{b}}$', fontsize=fontsize) + # plot predictions + if 'glacierwide_mb_mwea' in pred_primes.keys(): + mb_obs = obs['glacierwide_mb_mwea'] + axes[nparams].fill_between( + np.arange(len(ar)), + mb_obs[0] - (2 * mb_obs[1]), + mb_obs[0] + (2 * mb_obs[1]), + color='grey', + alpha=0.3, + ) + axes[nparams].fill_between( + np.arange(len(ar)), + mb_obs[0] - mb_obs[1], + mb_obs[0] + mb_obs[1], + color='grey', + alpha=0.3, + ) + + mb_primes = torch.stack(pred_primes['glacierwide_mb_mwea']).numpy() + mb_chain = torch.stack(pred_chain['glacierwide_mb_mwea']).numpy() + axes[nparams].plot(mb_primes, '.', ms=ms, c='tab:blue') + axes[nparams].plot(mb_chain, '.', ms=ms, c='tab:orange') + axes[nparams].plot( + [], + [], + label=f'median={np.median(mb_chain):.3f}\niqr={np.subtract(*np.percentile(mb_chain, [75, 25])):.3f}', + ) + ln2 = axes[nparams].legend(loc='upper right', handlelength=0, borderaxespad=0, fontsize=fontsize) + legs.append(ln2) + axes[nparams].set_ylabel(r'$\dot{{b}}$', fontsize=fontsize) + nparams += 1 + + # plot along-chain mean residual for all other prediction keys + for key in pred_primes.keys(): + if key == 'glacierwide_mb_mwea': + continue + + # stack predictions first (shape: n_steps x ... x ...) - may end up being 2d or 3d + pred_primes = torch.stack(pred_primes[key]).numpy() + pred_chain = torch.stack(pred_chain[key]).numpy() + + # flatten all axes except the first (n_steps) -> 2D array (n_steps, M) + pred_primes_flat = pred_primes.reshape(pred_primes.shape[0], -1) + pred_chain_flat = pred_chain.reshape(pred_chain.shape[0], -1) + + # make obs array broadcastable (flatten if needed) + obs_vals_flat = np.ravel(np.array(obs[key][0])) + + # compute mean residual per step + mean_resid_primes = np.nanmean(pred_primes_flat - obs_vals_flat, axis=1) + mean_resid_chain = np.nanmean(pred_chain_flat - obs_vals_flat, axis=1) + axes[nparams].plot(mean_resid_primes, '.', ms=ms, c='tab:blue') + axes[nparams].plot(mean_resid_chain, '.', ms=ms, c='tab:orange') + + if key == 'elev_change_1d': + axes[nparams].set_ylabel(r'$\overline{\hat{dh} - dh}$', fontsize=fontsize) + else: + axes[nparams].set_ylabel(r'$\overline{\mathrm{pred} - \mathrm{obs}}$', fontsize=fontsize) + legs.append(None) + nparams += 1 + + # axes[-1] will always be acceptance rate axes[-1].plot(ar, 'tab:orange', lw=1) axes[-1].plot( np.convolve(ar, np.ones(100) / 100, mode='valid'), @@ -234,7 +283,8 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show ax.xaxis.set_ticks_position('both') ax.yaxis.set_ticks_position('both') ax.tick_params(axis='both', direction='inout') - if i == n: + ax.yaxis.set_label_coords(-0.1, 0.5) + if i > m_primes.shape[1] - 1: continue ax.plot([], [], label=f'n_eff={neff[i]}') hands, ls = ax.get_legend_handles_labels() @@ -256,9 +306,8 @@ def plot_mcmc_chain(m_primes, m_chain, mb_obs, ar, title, ms=1, fontsize=8, show handlelength=0, fontsize=fontsize, ) - - for i, ax in enumerate(axes): - ax.add_artist(legs[i]) + if legs[i] is not None: + ax.add_artist(legs[i]) axes[0].set_xlim([0, m_chain.shape[0]]) axes[0].set_title(title, fontsize=fontsize) diff --git a/pygem/setup/config.py b/pygem/setup/config.py index c7fbff7f..f0def64a 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -163,7 +163,6 @@ def flatten_dict(d, parent_key=''): # Skip patterns if any(fnmatch.fnmatch(key, pat) for pat in skip_patterns): continue - print(key, value) path = os.path.join(root, value.strip(os.sep)) @@ -276,6 +275,7 @@ def flatten_dict(d, parent_key=''): 'calib.emulator_params.ftol_opt': float, 'calib.emulator_params.eps_opt': float, 'calib.MCMC_params': dict, + 'calib.MCMC_params.option_calib_glacierwide_mb_mwea': bool, 'calib.MCMC_params.option_use_emulator': bool, 'calib.MCMC_params.emulator_sims': int, 'calib.MCMC_params.tbias_step': (int, float), diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index dc382971..9610c4bd 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -150,6 +150,7 @@ calib: # MCMC params MCMC_params: + option_calib_glacierwide_mb_mwea: true # option to calibrate against average glacierwide mass balance data (true or false) option_use_emulator: true # use emulator or full model (if true, calibration must have first been run with option_calibretion=='emulator') emulator_sims: 100 tbias_step: 0.1 From 5bd47a5575eb41fd5cd8bc88d5e03a6259ecb0a3 Mon Sep 17 00:00:00 2001 From: "Brandon S. Tober" Date: Tue, 4 Nov 2025 14:43:48 -0500 Subject: [PATCH 18/19] Ingest and process 2d dhdt data (#158) Closes #152 * Allow user to specify their own mass balance dataset and field names Closes #156 * process 2d dhdt to glacierwide mass balance and 1d elev change profiles * Add dhdt_processing notebook to test suite * Reformat MCMC calibration framework to make more modular (#155) Closes #151 * set up obs and preds of mcmc as dictionary and match keys when calculating log likelihood of each step Closes #154 * Plot residual for any pred-obs pairs that aren't glacierwide mb, whether 2d or 1d --- pygem/bin/op/initialize.py | 2 +- .../run/run_calibration_frontalablation.py | 40 +- pygem/oggm_compat.py | 4 +- pygem/plot/graphics.py | 45 +- pygem/setup/config.py | 20 +- pygem/setup/config.yaml | 24 +- pygem/shop/elevchange1d.py | 145 +++++- pygem/shop/elevchange2d.py | 446 ++++++++++++++++++ pygem/shop/mbdata.py | 57 ++- pygem/tests/test_03_notebooks.py | 11 +- pygem/utils/_funcs.py | 44 ++ pyproject.toml | 2 +- 12 files changed, 758 insertions(+), 82 deletions(-) create mode 100644 pygem/shop/elevchange2d.py diff --git a/pygem/bin/op/initialize.py b/pygem/bin/op/initialize.py index 7b14826e..ee96e737 100644 --- a/pygem/bin/op/initialize.py +++ b/pygem/bin/op/initialize.py @@ -113,7 +113,7 @@ def main(): # Define the base directory basedir = os.path.join(os.path.expanduser('~'), 'PyGEM') # Google Drive file id for sample dataset - file_id = '16l2nEdWACwkpdNd8pdIX0ajyfGIsUf_B' + file_id = '1cRVG__7dVclut42LdQBjnXKpTvyYWBuK' # download and unzip out = download_and_unzip_from_google_drive(file_id, basedir) diff --git a/pygem/bin/run/run_calibration_frontalablation.py b/pygem/bin/run/run_calibration_frontalablation.py index 68c11511..fe53157f 100644 --- a/pygem/bin/run/run_calibration_frontalablation.py +++ b/pygem/bin/run/run_calibration_frontalablation.py @@ -873,13 +873,13 @@ def calib_ind_calving_k( frontalablation_fp='', frontalablation_fn='', output_fp='', - hugonnet2021_fp='', + massbalance_fp='', ): verbose = args.verbose overwrite = args.overwrite # Load calving glacier data fa_glac_data = pd.read_csv(frontalablation_fp + frontalablation_fn) - mb_data = pd.read_csv(hugonnet2021_fp) + mb_data = pd.read_csv(massbalance_fp) fa_glac_data['O1Region'] = [int(x.split('-')[1].split('.')[0]) for x in fa_glac_data.RGIId.values] calving_k_bndhigh_set = np.copy(calving_k_bndhigh_gl) @@ -2413,8 +2413,8 @@ def update_mbdata( regions=list(range(1, 20)), frontalablation_fp='', frontalablation_fn='', - hugonnet2021_fp='', - hugonnet2021_facorr_fp='', + massbalance_fp='', + massbalance_facorr_fp='', ncores=1, overwrite=False, verbose=False, @@ -2425,13 +2425,13 @@ def update_mbdata( ) fa_glac_data = pd.read_csv(frontalablation_fp + frontalablation_fn) # check if fa corrected mass balance data already exists - if os.path.exists(hugonnet2021_facorr_fp): + if os.path.exists(massbalance_facorr_fp): assert overwrite, ( - f'Frontal ablation corrected mass balance dataset already exists!\t{hugonnet2021_facorr_fp}\nPass `-o` to overwrite, or pass a different filename for `hugonnet2021_facorrected_fn`' + f'Frontal ablation corrected mass balance dataset already exists!\t{massbalance_facorr_fp}\nPass `-o` to overwrite, or pass a different filename for `massbalance_facorrected_fn`' ) - mb_data = pd.read_csv(hugonnet2021_facorr_fp) + mb_data = pd.read_csv(massbalance_facorr_fp) else: - mb_data = pd.read_csv(hugonnet2021_fp) + mb_data = pd.read_csv(massbalance_fp) mb_rgiids = list(mb_data.rgiid) # Record prior data @@ -2461,7 +2461,7 @@ def update_mbdata( ) # Export the updated dataset - mb_data.to_csv(hugonnet2021_facorr_fp, index=False) + mb_data.to_csv(massbalance_facorr_fp, index=False) # Update gdirs glac_strs = [] @@ -2701,18 +2701,18 @@ def main(): help='reference period ending year for calibration (typically 2019)', ) parser.add_argument( - '-hugonnet2021_fn', + '-massbalance_fn', action='store', type=str, - default=f'{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_fn"]}', - help='reference mass balance data file name (default: df_pergla_global_20yr-filled.csv)', + default=f'{pygem_prms["calib"]["data"]["massbalance"]["massbalance_fn"]}', + help='reference mass balance data file name (default taken from config.yaml)', ) parser.add_argument( - '-hugonnet2021_facorrected_fn', + '-massbalance_facorrected_fn', action='store', type=str, - default=f'{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_facorrected_fn"]}', - help='reference mass balance data file name (default: df_pergla_global_20yr-filled.csv)', + default=f'{pygem_prms["calib"]["data"]["massbalance"]["massbalance_facorrected_fn"]}', + help='reference mass balance data file name (default taken from config.yaml)', ) parser.add_argument( '-ncores', @@ -2748,8 +2748,8 @@ def main(): ) frontalablation_cal_fn = pygem_prms['calib']['data']['frontalablation']['frontalablation_cal_fn'] output_fp = frontalablation_fp + '/analysis/' - hugonnet2021_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}/{args.hugonnet2021_fn}' - hugonnet2021_facorr_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}/{args.hugonnet2021_facorrected_fn}' + massbalance_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["massbalance_relpath"]}/{args.massbalance_fn}' + massbalance_facorr_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["massbalance_relpath"]}/{args.massbalance_facorrected_fn}' os.makedirs(output_fp, exist_ok=True) # marge input calving datasets @@ -2766,7 +2766,7 @@ def main(): frontalablation_fp=frontalablation_fp, frontalablation_fn=merged_calving_data_fn, output_fp=output_fp, - hugonnet2021_fp=hugonnet2021_fp, + massbalance_fp=massbalance_fp, ) with multiprocessing.Pool(args.ncores) as p: p.map(calib_ind_calving_k_partial, args.rgi_region01) @@ -2784,8 +2784,8 @@ def main(): regions=args.rgi_region01, frontalablation_fp=output_fp, frontalablation_fn=frontalablation_cal_fn, - hugonnet2021_fp=hugonnet2021_fp, - hugonnet2021_facorr_fp=hugonnet2021_facorr_fp, + massbalance_fp=massbalance_fp, + massbalance_facorr_fp=massbalance_facorr_fp, ncores=args.ncores, overwrite=args.overwrite, verbose=args.verbose, diff --git a/pygem/oggm_compat.py b/pygem/oggm_compat.py index 7c08d4ee..264feb56 100755 --- a/pygem/oggm_compat.py +++ b/pygem/oggm_compat.py @@ -131,7 +131,7 @@ def single_flowline_glacier_directory( workflow.execute_entity_task(debris.debris_binned, gdir) # 1d elevation change calibration data if not os.path.isfile(gdir.get_filepath('elev_change_1d')): - workflow.execute_entity_task(elevchange1d.elev_change_1d_to_gdir, gdir) + workflow.execute_entity_task(elevchange1d.dh_1d_to_gdir, gdir) # 1d melt extent calibration data if not os.path.isfile(gdir.get_filepath('meltextent_1d')): workflow.execute_entity_task(meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir) @@ -234,7 +234,7 @@ def single_flowline_glacier_directory_with_calving( workflow.execute_entity_task(mbdata.mb_df_to_gdir, gdir, **{'facorrected': facorrected}) # 1d elevation change calibration data if not os.path.isfile(gdir.get_filepath('elev_change_1d')): - workflow.execute_entity_task(elevchange1d.elev_change_1d_to_gdir, gdir) + workflow.execute_entity_task(elevchange1d.dh_1d_to_gdir, gdir) # 1d melt extent calibration data if not os.path.isfile(gdir.get_filepath('meltextent_1d')): workflow.execute_entity_task(meltextent_and_snowline_1d.meltextent_1d_to_gdir, gdir) diff --git a/pygem/plot/graphics.py b/pygem/plot/graphics.py index 9bd168d6..7194dc6a 100644 --- a/pygem/plot/graphics.py +++ b/pygem/plot/graphics.py @@ -19,6 +19,44 @@ from pygem.utils.stats import effective_n +def plot_elev_change_1d(data_dict, figsize=(5, 3), title=None): + """ + Plot 1D elevation change profiles with error bars. + + Parameters + ---------- + data_dict : dict + Dictionary containing keys: + - 'bin_edges': array-like, used to derive bin centers + - 'dh': list of arrays, one per date pair + - 'dh_sigma': list of arrays, same shape as dh + - 'dates': list of [t1, t2] pairs + figsize : tuple + Figure size. + title : str or None + Optional title. + """ + bin_edges = np.array(data_dict['bin_edges']) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 # midpoint of each bin + dh_list = data_dict['dh'] + sigma_list = data_dict['dh_sigma'] + dates = data_dict['dates'] + + fig, ax = plt.subplots(1, 1, figsize=figsize) + + for dh, sigma, date_pair in zip(dh_list, sigma_list, dates): + label = f'{date_pair[0]}_{date_pair[1]}' + ax.errorbar(bin_centers, dh, yerr=sigma, label=label, marker='o', linestyle='-', capsize=3) + + ax.set_xlabel('Elevation (m)') + ax.set_ylabel('Elevation change (m)') + ax.legend() + if title: + ax.set_title(title) + plt.tight_layout() + plt.show() + + def plot_modeloutput_section( model=None, ax=None, @@ -30,13 +68,6 @@ def plot_modeloutput_section( ): """Plots the result of the model output along the flowline. A paired down version of OGGMs graphics.plot_modeloutput_section() - - Parameters - ---------- - model: obj - either a FlowlineModel or a list of model flowlines. - fig - title """ try: diff --git a/pygem/setup/config.py b/pygem/setup/config.py index f0def64a..0666329d 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -111,7 +111,6 @@ def _validate_config(self, config): '*meltextent*', '*dh_1d*', '*dhdt_2d*', - '*elev_change_1d*', ] # --- Type validation (existing code) --- @@ -316,16 +315,25 @@ def flatten_dict(d, parent_key=''): 'calib.MCMC_params.option_calib_snowline_1d': bool, 'calib.data': dict, 'calib.data.massbalance': dict, - 'calib.data.massbalance.hugonnet2021_relpath': str, - 'calib.data.massbalance.hugonnet2021_fn': str, - 'calib.data.massbalance.hugonnet2021_facorrected_fn': str, + 'calib.data.massbalance.massbalance_relpath': str, + 'calib.data.massbalance.massbalance_fn': str, + 'calib.data.massbalance.massbalance_facorrected_fn': str, + 'calib.data.massbalance.massbalance_rgiid_colname': str, + 'calib.data.massbalance.massbalance_mb_colname': str, + 'calib.data.massbalance.massbalance_mb_error_colname': str, + 'calib.data.massbalance.massbalance_mb_clim_colname': (str, type(None)), + 'calib.data.massbalance.massbalance_mb_clim_error_colname': (str, type(None)), + 'calib.data.massbalance.massbalance_period_colname': str, + 'calib.data.massbalance.massbalance_period_date_format': str, + 'calib.data.massbalance.massbalance_period_delimiter': str, 'calib.data.frontalablation': dict, 'calib.data.frontalablation.frontalablation_relpath': str, 'calib.data.frontalablation.frontalablation_cal_fn': str, 'calib.data.icethickness': dict, 'calib.data.icethickness.h_ref_relpath': str, - 'calib.data.elev_change_1d': dict, - 'calib.data.elev_change_1d.elev_change_1d_relpath': (str, type(None)), + 'calib.data.elev_change': dict, + 'calib.data.elev_change.dhdt_2d_relpath': (str, type(None)), + 'calib.data.elev_change.dh_1d_relpath': (str, type(None)), 'calib.data.meltextent_1d': dict, 'calib.data.meltextent_1d.meltextent_1d_relpath': (str, type(None)), 'calib.data.snowline_1d': dict, diff --git a/pygem/setup/config.yaml b/pygem/setup/config.yaml index 9610c4bd..97e838cf 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -199,11 +199,20 @@ calib: # calibration datasets data: - # mass balance data + # mass balance data - assumed to be in units of m w.e. yr-1 massbalance: - hugonnet2021_relpath: /DEMs/Hugonnet2021/ # relative to main data path - hugonnet2021_fn: df_pergla_global_20yr-filled.csv # this file is 'raw', filled geodetic mass balance from Hugonnet et al. (2021) - pulled by prerproc_fetch_mbdata.py - hugonnet2021_facorrected_fn: df_pergla_global_20yr-filled-frontalablation-corrected.csv # frontal ablation corrected geodetic mass balance (produced by run_calibration_frontalablation.py) + massbalance_relpath: massbalance_data/Hugonnet2021/ # relative to main data path + massbalance_fn: df_pergla_global_20yr-filled.csv # this file is 'raw', filled geodetic mass balance from Hugonnet et al. (2021) - pulled by prerproc_fetch_mbdata.py + massbalance_facorrected_fn: df_pergla_global_20yr-filled-frontalablation-corrected.csv # frontal ablation corrected geodetic mass balance (produced by run_calibration_frontalablation.py) + # mass balance dataset fields names and formats + massbalance_rgiid_colname: rgiid # RGI glacier ID column name + massbalance_mb_colname: mb_mwea # mass balance column name + massbalance_mb_error_colname: mb_mwea_err # mass balance error column name + massbalance_mb_clim_colname: mb_clim_mwea # climatic mass balance column name + massbalance_mb_clim_error_colname: mb_clim_mwea_err # climatic mass balance error column name + massbalance_period_colname: period # mass balance period column name + massbalance_period_date_format: YYYY-MM-DD # mass balance period date format + massbalance_period_delimiter: _ # mass balance period deimiter (separating start and end dates) # frontal ablation frontalablation: frontalablation_relpath: /frontalablation_data/ # relative to main data path @@ -211,9 +220,10 @@ calib: # ice thickness icethickness: h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ - # 1d elevation change - elev_change_1d: - elev_change_1d_relpath: /elev_change_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 01.00570_elev_change_1d.json) + # elevation change - 2d dhdt or 1d elev change profiles + elev_change: + dhdt_2d_relpath: elev_change_data/dhdt_2d/ # relative to main data path. per-glacier files within will be expected (_YYYY-MM-DD_YYYY-MM-DD_dhdt_2d.tif) or may be processed to each glacier directory with shop.dhdt2d.dhdt2d_to_gdir() + dh_1d_relpath: elev_change_data/dh_1d/ # relative to main data path. per-glacier files within will be expected as _elev_change_1d_.json (e.g., 1.00570_dh_1d_.json) # 1d melt extents meltextent_1d: meltextent_1d_relpath: /SAR_data/merged/ # relative to main data path. per-glacier files within will be expected as _melt_extent_elev.csv (e.g., 01.00570_melt_extent_elev.csv) diff --git a/pygem/shop/elevchange1d.py b/pygem/shop/elevchange1d.py index 4bafd7a7..6d1abc96 100644 --- a/pygem/shop/elevchange1d.py +++ b/pygem/shop/elevchange1d.py @@ -19,6 +19,7 @@ # Local libraries from oggm import cfg from oggm.utils import entity_task +from scipy.stats import binned_statistic # pygem imports from pygem.setup.config import ConfigManager @@ -41,8 +42,14 @@ @entity_task(log, writes=['elev_change_1d']) -def elev_change_1d_to_gdir( +def dh_1d_to_gdir( gdir, + dh_datadir=f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["elev_change"]["dh_1d_relpath"]}/', + infile_suffix='', + bin_spacing=None, + bin_lowcut=None, + bin_highcut=None, + bin_cut_percentile=True, ): """ Add 1D elevation change observations to the given glacier directory. @@ -111,14 +118,22 @@ def elev_change_1d_to_gdir( ---------- gdir : :py:class:`oggm.GlacierDirectory` where to write the data - + dh_datadir : str + Directory where binned elevation change data files are stored + infile_suffix : str + Optional suffix at following "elev_change_1d" in input file name + bin_spacing : float or None + If provided, rebin the elevation change data to this bin spacing (in meters) + bin_lowcut : float or None + If provided, cut elevation bins below this elevation (meters or percentile) + bin_highcut : float or None + If provided, cut elevation bins above this elevation (meters or percentile) + bin_cut_percentile : bool + If True (default), interpret bin_lowcut and bin_highcut as percentiles of the elevation range. + If False, interpret as absolute elevation values (in meters). """ # get dataset file path - elev_change_1d_fp = ( - f'{pygem_prms["root"]}/' - f'{pygem_prms["calib"]["data"]["elev_change_1d"]["elev_change_1d_relpath"]}/' - f'{gdir.rgi_id.split("-")[1]}_elev_change_1d' - ) + elev_change_1d_fp = f'{dh_datadir}/{gdir.rgi_id.split("-")[1]}_elev_change_1d{infile_suffix}' # Check for both .json and .csv extensions if os.path.exists(elev_change_1d_fp + '.json'): @@ -136,6 +151,17 @@ def elev_change_1d_to_gdir( validate_elev_change_1d_structure(data) + # optionally cut bins + if bin_lowcut is not None or bin_highcut is not None: + data = filter_elev_change_1d_data(data, bin_lowcut, bin_highcut, bin_cut_percentile) + + # optionally rebin + if bin_spacing: + data = rebin_elev_change_1d_data(data, float(bin_spacing)) + + # can't hurt to validate again after rebinning + validate_elev_change_1d_structure(data) + gdir.write_json(data, 'elev_change_1d') @@ -254,11 +280,15 @@ def csv_to_elev_change_1d_dict(csv_path): # Ensure sorted bins df = df.sort_values(['bin_start', 'date_start', 'date_end']).reset_index(drop=True) - # Get all unique bin edges - bin_edges = sorted(set(df['bin_start']).union(df['bin_stop'])) + # Drop duplicate (bin_start, bin_stop) pairs + df_unique_bins = df.drop_duplicates(subset=['bin_start', 'bin_stop']).sort_values('bin_start') + + # Define bin edges from unique bins + bin_edges = np.concatenate(([df_unique_bins['bin_start'].iloc[0]], df_unique_bins['bin_stop'].values)).tolist() - if 'bin_area' in df.keys(): - bin_area = df['bin_area'].tolist() + # Handle bin_area if present + if 'bin_area' in df.columns: + bin_area = df_unique_bins['bin_area'].tolist() else: bin_area = False @@ -305,3 +335,96 @@ def csv_to_elev_change_1d_dict(csv_path): del data['bin_area'] return data + + +def rebin_elev_change_1d_data(data, bin_spacing): + """ + Rebin elevation change data to new bin spacing. + """ + bin_centers = data['bin_centers'] + # estimate bin width (assuming uniform) + dz = np.abs(np.diff(bin_centers).mean()) + + # optionally rebin + if bin_spacing != dz: + # define new bin edges + new_edges = np.arange(min(data['bin_edges']), max(data['bin_edges']) + bin_spacing, bin_spacing) + + # simple mean of dh per new bin + dh_rebinned, _, _ = binned_statistic(bin_centers, data['dh'], statistic='mean', bins=new_edges) + + # simple mean of dh_sigma per new bin + dh_sigma_rebinned, _, _ = binned_statistic(bin_centers, data['dh_sigma'], statistic='mean', bins=new_edges) + + # sum of bin_area per new bin + bin_area_rebinned, _, _ = binned_statistic(bin_centers, data['bin_area'], statistic='sum', bins=new_edges) + + # replace data values + data['bin_edges'] = new_edges.tolist() + data['bin_centers'] = [ + 0.5 * (data['bin_edges'][i] + data['bin_edges'][i + 1]) for i in range(len(data['bin_edges']) - 1) + ] + data['bin_area'] = bin_area_rebinned.tolist() + data['dh'] = dh_rebinned.tolist() + data['dh_sigma'] = dh_sigma_rebinned.tolist() + return data + + +def filter_elev_change_1d_data(data, bin_lowcut, bin_highcut, bin_cut_percentile): + """ + Filter elevation change data to only include bins within specified elevation range. + + Parameters + ---------- + data : dict + Elevation change data dictionary. + bin_lowcut : float or None + Lower elevation cut-off. If None, no lower cut is applied. + bin_highcut : float or None + Upper elevation cut-off. If None, no upper cut is applied. + bin_cut_percentile : bool + If True, interpret low/high cut as percentiles of the elevation distribution. + + Returns + ------- + data_filtered : dict + Filtered elevation change data dictionary. + """ + bin_centers = np.array(data['bin_centers']) + bin_edges = np.array(data['bin_edges']) + + if bin_cut_percentile: + if bin_lowcut is not None: + bin_lowcut = np.percentile(bin_centers, bin_lowcut) + if bin_highcut is not None: + bin_highcut = np.percentile(bin_centers, bin_highcut) + + mask = np.ones_like(bin_centers, dtype=bool) + if bin_lowcut is not None: + mask &= bin_centers >= bin_lowcut + if bin_highcut is not None: + mask &= bin_centers <= bin_highcut + + # Ensure at least one bin remains + if not np.any(mask): + raise ValueError('No bins remain after filtering.') + + # Select corresponding edges (N+1) + idx = np.where(mask)[0] + bin_edges_filtered = bin_edges[idx[0] : idx[-1] + 2] + + data_filtered = data.copy() + data_filtered['bin_centers'] = bin_centers[mask].tolist() + data_filtered['bin_edges'] = bin_edges_filtered.tolist() + + if 'bin_area' in data: + data_filtered['bin_area'] = np.array(data['bin_area'])[mask].tolist() + + data_filtered['dh'] = [np.array(dh_arr)[mask].tolist() for dh_arr in data['dh']] + + if isinstance(data['dh_sigma'], list): + data_filtered['dh_sigma'] = [np.array(sigma_arr)[mask].tolist() for sigma_arr in data['dh_sigma']] + else: + data_filtered['dh_sigma'] = data['dh_sigma'] + + return data_filtered diff --git a/pygem/shop/elevchange2d.py b/pygem/shop/elevchange2d.py new file mode 100644 index 00000000..ac38fc3a --- /dev/null +++ b/pygem/shop/elevchange2d.py @@ -0,0 +1,446 @@ +""" +Python Glacier Evolution Model (PyGEM) + +copyright © 2025 Brandon Tober , David Rounce + +Distributed under the MIT license + +Raster reprojection and processing to glacier directory framework adapted from the Open Global Glacier Model (OGGM) shop/hugonnet_maps.py module. +""" + +import glob +import logging +import os +import re +import warnings + +import numpy as np +import pandas as pd +import xarray as xr +from packaging.version import Version + +try: + import rasterio + from rasterio import MemoryFile + from rasterio.warp import Resampling, calculate_default_transform, reproject + + try: + # rasterio V > 1.0 + from rasterio.merge import merge as merge_tool + except ImportError: + from rasterio.tools.merge import merge as merge_tool +except ImportError: + pass +import geopandas as gpd +from oggm import cfg, tasks, utils +from shapely.geometry import box + +from pygem.setup.config import ConfigManager +from pygem.utils._funcs import parse_period + +config_manager = ConfigManager() +# read the config +pygem_prms = config_manager.read_config() + + +# Module logger +log = logging.getLogger(__name__) + +data_basedir = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["elev_change"]["dhdt_2d_relpath"]}/' + + +def raster_overlaps_glacier(raster_path, glacier_geom): + """Return True if raster overlaps glacier extent (reprojects if needed).""" + with rasterio.open(raster_path) as src: + geom = box(*src.bounds) + if src.crs.to_string() != 'EPSG:4326': + geom = gpd.GeoSeries([geom], crs=src.crs).to_crs('EPSG:4326').iloc[0] + return geom.intersects(glacier_geom) + + +@utils.entity_task(log, writes=['gridded_data']) +def dhdt_to_gdir( + gdir, + raster_path=None, + period='', + t1='', + t2='', + date_format=pygem_prms['calib']['data']['massbalance']['massbalance_period_date_format'], + period_delimiter=pygem_prms['calib']['data']['massbalance']['massbalance_period_delimiter'], + gridded_data_suffix='', + verbose=False, +): + """Add 2d dhdt data to this glacier directory. + + Parameters + ---------- + gdir : :py:class:`oggm.GlacierDirectory` + the glacier directory to process + raster_path : str, optional + A path to a single raster file or a directory containing raster files. + If None, defaults to the standard data_basedir. + t1 : str, optional + A string indicating the start date of the dhdt data (e.g. '2000-01-01'). + t2 : str, optional + A string indicating the end date of the dhdt data (e.g. '2020-01-01'). + date_format : str, optional + A string indicating the date format used in t1, t2, and period (e.g. 'YYYY-MM-DD'). + period_delimiter : str, optional + A string indicating the delimiter used in the period string (e.g. '_'). + period: str, optional + A string indicating the time period of the dhdt data (e.g. '2000-01-01_2020-01-01'). + verbose : bool, optional + """ + # step 0. determine base directory + if raster_path is None: + basedir = os.path.normpath(data_basedir) + elif os.path.isfile(raster_path): + # single file + tif_files = [raster_path] + basedir = os.path.dirname(raster_path) + elif os.path.isdir(raster_path): + basedir = raster_path + tif_files = glob.glob(os.path.join(basedir, '*.tif')) + else: + raise ValueError(f"Provided raster_path '{raster_path}' is not valid.") + + # step 1. get glacier bounds + lon_ex, lat_ex = gdir.extent_ll + lon_ex = [np.floor(lon_ex[0]) - 1e-9, np.ceil(lon_ex[1]) + 1e-9] + lat_ex = [np.floor(lat_ex[0]) - 1e-9, np.ceil(lat_ex[1]) + 1e-9] + # define glacier bounding box + glacier_bounds = box(lon_ex[0], lat_ex[0], lon_ex[1], lat_ex[1]) + + # step 2. if raster_path was a directory or None, look for tif files + if raster_path is None or os.path.isdir(raster_path): + RO1_basedir = os.path.join(basedir, f'{gdir.rgi_region.zfill(2)}') + if os.path.isdir(RO1_basedir): + tif_files = glob.glob(os.path.join(RO1_basedir, '*.tif')) + else: + tif_files = glob.glob(os.path.join(basedir, '*.tif')) + + if not tif_files: + log.info(f'No rasters found for glacier {gdir.rgi_id}') + return + + # match files by rgi_id if possible + glac_no = '{0:0.5f}'.format(float(gdir.rgi_id.split('-')[1])) + flist = [ + f + for f in tif_files + if (gdir.rgi_id in os.path.basename(f) or glac_no in os.path.basename(f)) + and raster_overlaps_glacier(f, glacier_bounds) + ] + + # if no RGI ID match, look for any raster overlapping glacier extent + if not flist: + flist = [f for f in tif_files if raster_overlaps_glacier(f, glacier_bounds)] + + if verbose: + print(f'Found {len(flist)} files for {gdir.rgi_id}:', [f.split('/')[-1] for f in flist]) + + if not flist: + log.info(f'No rasters overlap glacier {gdir.rgi_id}') + return + + # search for date strings in files + pattern = r'\d{4}-\d{2}-\d{2}_\d{4}-\d{2}-\d{2}' + matches = list({re.search(pattern, f).group(0) for f in flist if re.search(pattern, f)}) + + if matches: + if len(set(matches)) != 1: + raise ValueError( + f'It seems the dhdt files for glacier {gdir.rgi_id} may cover different date ranges: {set(matches)}' + ) + period = matches[0] + + # notmalize user-input formats like YYYY-MM-DD -> %Y-%m-%d + date_format = date_format.replace('YYYY', '%Y').replace('YY', '%y').replace('MM', '%m').replace('DD', '%d') + + if t1 and t2: + t1 = pd.to_datetime(t1, format=date_format) + t2 = pd.to_datetime(t2, format=date_format) + elif period: + t1, t2 = parse_period(period, date_format=date_format, delimiter=period_delimiter) + + if verbose: + print('Dataset time period:\t{t1} to {t2}'.format(t1=t1.strftime('%Y-%m-%d'), t2=t2.strftime('%Y-%m-%d'))) + + # A glacier area can cover more than one tile: + if len(flist) == 1: + dem_dss = [rasterio.open(flist[0])] # if one tile, just open it + file_crs = dem_dss[0].crs + dem_data = rasterio.band(dem_dss[0], 1) + if Version(rasterio.__version__) >= Version('1.0'): + src_transform = dem_dss[0].transform + else: + src_transform = dem_dss[0].affine + nodata = dem_dss[0].meta.get('nodata', None) + else: + dem_dss = [rasterio.open(s) for s in flist] # list of rasters + + # make sure all files have the same crs and reproject if needed; + # defining the target crs to the one most commonly used, minimizing + # the number of files for reprojection + crs_list = np.array([dem_ds.crs.to_string() for dem_ds in dem_dss]) + unique_crs, crs_counts = np.unique(crs_list, return_counts=True) + file_crs = rasterio.crs.CRS.from_string(unique_crs[np.argmax(crs_counts)]) + + if len(unique_crs) != 1: + # more than one crs, we need to do reprojection + memory_files = [] + for i, src in enumerate(dem_dss): + if file_crs != src.crs: + transform, width, height = calculate_default_transform( + src.crs, file_crs, src.width, src.height, *src.bounds + ) + kwargs = src.meta.copy() + kwargs.update({'crs': file_crs, 'transform': transform, 'width': width, 'height': height}) + + reprojected_array = np.empty(shape=(src.count, height, width), dtype=src.dtypes[0]) + # just for completeness; even the data only has one band + for band in range(1, src.count + 1): + reproject( + source=rasterio.band(src, band), + destination=reprojected_array[band - 1], + src_transform=src.transform, + src_crs=src.crs, + dst_transform=transform, + dst_crs=file_crs, + resampling=Resampling.nearest, + ) + + memfile = MemoryFile() + with memfile.open(**kwargs) as mem_dst: + mem_dst.write(reprojected_array) + memory_files.append(memfile) + else: + memfile = MemoryFile() + with memfile.open(**src.meta) as mem_src: + mem_src.write(src.read()) + memory_files.append(memfile) + + with rasterio.Env(): + datasets_to_merge = [memfile.open() for memfile in memory_files] + nodata = datasets_to_merge[0].meta.get('nodata', None) + dem_data, src_transform = merge_tool(datasets_to_merge, nodata=nodata) + # close datasets + for mf in memory_files: + mf.close() + for ds_merge in datasets_to_merge: + ds_merge.close() + else: + # only one single crs occurring, no reprojection needed + nodata = dem_dss[0].meta.get('nodata', None) + dem_data, src_transform = merge_tool(dem_dss, nodata=nodata) + + # Set up profile for writing output + with rasterio.open(gdir.get_filepath('dem')) as dem_ds: + dst_array = dem_ds.read().astype(np.float32) + dst_array[:] = np.nan + profile = dem_ds.profile + transform = dem_ds.transform + dst_crs = dem_ds.crs + profile.update( + { + 'nodata': np.nan, + } + ) + + resampling = Resampling.bilinear + + with MemoryFile() as dest: + reproject( + # Source parameters + source=dem_data, + src_crs=file_crs, + src_transform=src_transform, + src_nodata=nodata, + # Destination parameters + destination=dst_array, + dst_transform=transform, + dst_crs=dst_crs, + dst_nodata=np.nan, + # Configuration + resampling=resampling, + ) + dest.write(dst_array) + + for dem_ds in dem_dss: + dem_ds.close() + + # Write + with utils.ncDataset(gdir.get_filepath('gridded_data'), 'a') as nc: + vn = 'dhdt' + gridded_data_suffix + if vn in nc.variables: + v = nc.variables[vn] + else: + v = nc.createVariable( + vn, + 'f4', + ( + 'y', + 'x', + ), + zlib=True, + fill_value=np.nan, + ) + v.units = 'm' + ln = 'dhdt' + v.long_name = ln + data_str = ' '.join(flist) if len(flist) > 1 else flist[0] + v.data_source = data_str + v.period = period + v.t1 = t1.strftime('%Y-%m-%d') if t1 else '' + v.t2 = t2.strftime('%Y-%m-%d') if t2 else '' + v[:] = np.squeeze(dst_array).astype(np.float32) + + +@utils.entity_task(log) +def dhdt_statistics(gdir, compute_massbalance=True, gridded_data_suffix=''): + """Gather statistics about the dhdt data.""" + + d = dict() + + # Easy stats - this should always be possible + d['rgi_id'] = gdir.rgi_id + d['rgi_region'] = gdir.rgi_region + d['rgi_subregion'] = gdir.rgi_subregion + d['rgi_area_km2'] = gdir.rgi_area_km2 + d['area_km2'] = 0 + d['perc_cov'] = 0 + d['avg_dhdt'] = np.nan + + try: + with xr.open_dataset(gdir.get_filepath('gridded_data')) as ds: + dhdt = ds['dhdt' + gridded_data_suffix].where(ds['glacier_mask'], np.nan).load() + gridded_area = ds['glacier_mask'].sum() * gdir.grid.dx**2 * 1e-6 + d['area_km2'] = float((~dhdt.isnull()).sum() * gdir.grid.dx**2 * 1e-6) + d['perc_cov'] = float(d['area_km2'] / gridded_area) + with warnings.catch_warnings(): + # This can trigger an empty mean warning + warnings.filterwarnings('ignore', category=RuntimeWarning) + d['avg_dhdt'] = np.nanmean(dhdt.data) + if compute_massbalance: + # convert to m w.e. yr^-1 + d['dmdtda'] = ( + d['avg_dhdt'] * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] + ) + d['period'] = ds['dhdt' + gridded_data_suffix].attrs['period'] + d['t1'] = ds['dhdt' + gridded_data_suffix].attrs['t1'] + d['t2'] = ds['dhdt' + gridded_data_suffix].attrs['t2'] + except (FileNotFoundError, AttributeError, KeyError): + pass + + return d + + +@utils.global_task(log) +def compile_hugonnet_statistics(gdirs, filesuffix='', path=True): + """Gather as much statistics as possible about a list of glaciers. + + It can be used to do result diagnostics and other stuffs. + + Parameters + ---------- + gdirs : list of :py:class:`oggm.GlacierDirectory` objects + the glacier directories to process + filesuffix : str + add suffix to output file + path : str, bool + Set to "True" in order to store the info in the working directory + Set to a path to store the file to your chosen location + """ + from oggm.workflow import execute_entity_task + + out_df = execute_entity_task(dhdt_statistics, gdirs) + + out = pd.DataFrame(out_df).set_index('rgi_id') + + if path: + if path is True: + out.to_csv(os.path.join(cfg.PATHS['working_dir'], ('dhdt_statistics' + filesuffix + '.csv'))) + else: + out.to_csv(path) + + return out + + +def dh_1d( + gdir, + dhdt_error=None, + ref_dem_year=None, + outdir=(f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["elev_change"]["dh_1d_relpath"]}/'), + gridded_data_suffix='', + outfile_suffix='', +): + """Convert the 2d dhdt data to a 1d elevation change profile and save to gdir. + + Parameters + ---------- + gdir : :py:class:`oggm.GlacierDirectory` + the glacier directory to process + """ + tasks.elevation_band_flowline( + gdir, + bin_variables=['dhdt' + gridded_data_suffix], + ) + df = pd.read_csv(gdir.get_filepath('elevation_band_flowline'), index_col=0) + + # get dhdt time period + with xr.open_dataset(gdir.get_filepath('gridded_data')) as ds: + ds = ds.load() + t2 = pd.to_datetime(ds['dhdt' + gridded_data_suffix].attrs['t2']) + t1 = pd.to_datetime(ds['dhdt' + gridded_data_suffix].attrs['t1']) + nyrs = (t2 - t1).days / 365.25 + + # compute bin edges + bin_centers = df['bin_elevation'].values + + # estimate bin width (assuming uniform) + dz = np.diff(bin_centers).mean() + + # compute edges + bin_edges = np.concatenate(([bin_centers[0] - dz / 2], bin_centers[:-1] + dz / 2, [bin_centers[-1] + dz / 2])) + + # get bin start + bin_start = bin_edges[:-1] + # get bin end + bin_end = bin_edges[1:] + # get bin area + bin_area = df['area'] + # compute dh - dhdt * nyears + dh = df['dhdt' + gridded_data_suffix] * nyrs + # get gdir ref dem + ref_dem = gdir.dem_info.split('\n')[0] + # need a reference dem year that the data was binned to for proper dynamic model calibration. + # default oggm is COP90, which has variable reference year depending on region + if not ref_dem_year: + match = list(set(re.findall(r'\b\d{4}-\d{4}\b', gdir.dem_info)))[0] + if match: + ref_dem_year = round(sum(int(y) for y in match.split('-')) / 2) + if not ref_dem_year: + raise ValueError(f'Could not determine reference DEM year from gdir.dem_info: {gdir.dem_info}') + + if not dhdt_error: + raise ValueError('dhdt_error must be provided to compute elevation change uncertainty.') + dh_error = dhdt_error * nyrs + # save as csv + out_df = pd.DataFrame( + { + 'bin_centers': bin_centers, + 'bin_start': bin_start, + 'bin_stop': bin_end, + 'bin_area': bin_area, + 'date_start': t1.strftime('%Y-%m-%d'), + 'date_end': t2.strftime('%Y-%m-%d'), + 'dh': dh, + 'dh_sigma': dh_error, + 'ref_dem': ref_dem, + 'ref_dem_year': ref_dem_year, + } + ) + + outfpath = os.path.join(os.path.normpath(outdir), f'{gdir.rgi_id.split("-")[1]}_elev_change_1d{outfile_suffix}.csv') + out_df.to_csv(outfpath, index=False) + return out_df diff --git a/pygem/shop/mbdata.py b/pygem/shop/mbdata.py index a507dfba..944b8ea7 100755 --- a/pygem/shop/mbdata.py +++ b/pygem/shop/mbdata.py @@ -15,17 +15,12 @@ import numpy as np import pandas as pd - -# import rasterio -# import xarray as xr -# Local libraries from oggm import cfg from oggm.utils import entity_task -# from oggm.core.gis import rasterio_to_gdir -# from oggm.utils import ncDataset # pygem imports from pygem.setup.config import ConfigManager +from pygem.utils._funcs import parse_period # instantiate ConfigManager config_manager = ConfigManager() @@ -47,7 +42,6 @@ @entity_task(log, writes=['mb_calib_pygem']) def mb_df_to_gdir( gdir, - mb_dataset='Hugonnet2021', facorrected=pygem_prms['setup']['include_frontalablation'], ): """Select specific mass balance and add observations to the given glacier directory @@ -57,20 +51,25 @@ def mb_df_to_gdir( gdir : :py:class:`oggm.GlacierDirectory` where to write the data """ - # get dataset name (could potentially be swapped with others besides Hugonnet21) - mbdata_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["hugonnet2021_relpath"]}' - mbdata_fp_fa = mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_facorrected_fn'] + # get dataset filepath + mbdata_fp = f'{pygem_prms["root"]}/{pygem_prms["calib"]["data"]["massbalance"]["massbalance_relpath"]}' + mbdata_fp_fa = mbdata_fp + pygem_prms['calib']['data']['massbalance']['massbalance_facorrected_fn'] if facorrected and os.path.exists(mbdata_fp_fa): mbdata_fp = mbdata_fp_fa else: - mbdata_fp = mbdata_fp + pygem_prms['calib']['data']['massbalance']['hugonnet2021_fn'] + mbdata_fp = mbdata_fp + pygem_prms['calib']['data']['massbalance']['massbalance_fn'] assert os.path.exists(mbdata_fp), 'Error, mass balance dataset does not exist: {mbdata_fp}' - rgiid_cn = 'rgiid' - mb_cn = 'mb_mwea' - mberr_cn = 'mb_mwea_err' - mb_clim_cn = 'mb_clim_mwea' - mberr_clim_cn = 'mb_clim_mwea_err' + + # get column names and formats + rgiid_cn = pygem_prms['calib']['data']['massbalance']['massbalance_rgiid_colname'] + mb_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_colname'] + mberr_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_error_colname'] + mb_clim_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_clim_colname'] + mberr_clim_cn = pygem_prms['calib']['data']['massbalance']['massbalance_mb_clim_error_colname'] + massbalance_period_colname = pygem_prms['calib']['data']['massbalance']['massbalance_period_colname'] + massbalance_period_date_format = pygem_prms['calib']['data']['massbalance']['massbalance_period_date_format'] + massbalance_period_delimiter = pygem_prms['calib']['data']['massbalance']['massbalance_period_delimiter'] # read reference mass balance dataset and pull data of interest mb_df = pd.read_csv(mbdata_fp) @@ -84,16 +83,30 @@ def mb_df_to_gdir( mb_mwea = mb_df.loc[rgiid_idx, mb_cn] mb_mwea_err = mb_df.loc[rgiid_idx, mberr_cn] - if mb_clim_cn in mb_df.columns: + if ( + mb_clim_cn is not None + and mberr_clim_cn is not None + and all(col in mb_df.columns for col in [mb_clim_cn, mberr_clim_cn]) + ): mb_clim_mwea = mb_df.loc[rgiid_idx, mb_clim_cn] mb_clim_mwea_err = mb_df.loc[rgiid_idx, mberr_clim_cn] else: mb_clim_mwea = None mb_clim_mwea_err = None - t1_str, t2_str = mb_df.loc[rgiid_idx, 'period'].split('_') - t1_datetime = pd.to_datetime(t1_str) - t2_datetime = pd.to_datetime(t2_str) + # notmalize user-input formats like YYYY-MM-DD -> %Y-%m-%d + massbalance_period_date_format = ( + massbalance_period_date_format.replace('YYYY', '%Y') + .replace('YY', '%y') + .replace('MM', '%m') + .replace('DD', '%d') + ) + + t1_datetime, t2_datetime = parse_period( + mb_df.loc[rgiid_idx, massbalance_period_colname], + date_format=massbalance_period_date_format, + delimiter=massbalance_period_delimiter, + ) # remove one day from t2 datetime for proper indexing (ex. 2001-01-01 want to run through 2000-12-31) t2_datetime = t2_datetime - timedelta(days=1) @@ -108,8 +121,8 @@ def mb_df_to_gdir( 'mb_mwea_err': float(mb_mwea_err), 'mb_clim_mwea': float(mb_clim_mwea) if mb_clim_mwea is not None else None, 'mb_clim_mwea_err': float(mb_clim_mwea_err) if mb_clim_mwea_err is not None else None, - 't1_str': t1_str, - 't2_str': t2_str, + 't1_str': t1_datetime.strftime('%Y-%m-%d'), + 't2_str': t2_datetime.strftime('%Y-%m-%d'), 'nyears': nyears, }.items() if value is not None diff --git a/pygem/tests/test_03_notebooks.py b/pygem/tests/test_03_notebooks.py index 872f2084..6af257ba 100644 --- a/pygem/tests/test_03_notebooks.py +++ b/pygem/tests/test_03_notebooks.py @@ -16,12 +16,13 @@ # TODO #54: Test all notebooks # notebooks = [f for f in os.listdir(nb_dir) if f.endswith('.ipynb')] -# list of notebooks to test, in the desired order +# list of notebooks to test, in the desired order (failures may occur if order is changed) notebooks = [ - 'simple_test.ipynb', - 'advanced_test.ipynb', - 'advanced_test_spinup_elev_change_calib.ipynb', - 'advanced_test_tw.ipynb', + 'simple_test.ipynb', # runs with sample_data + 'advanced_test.ipynb', # runs with sample_data + 'dhdt_processing.ipynb', # runs with sample_data + 'advanced_test_spinup_elev_change_calib.ipynb', # runs with sample_data, depends on dhdt_processing.ipynb results + 'advanced_test_tw.ipynb', # runs with sample_data_tw ] diff --git a/pygem/utils/_funcs.py b/pygem/utils/_funcs.py index ae57bf8a..79ecc5f1 100755 --- a/pygem/utils/_funcs.py +++ b/pygem/utils/_funcs.py @@ -12,6 +12,7 @@ import json import numpy as np +import pandas as pd from scipy.interpolate import interp1d from pygem.setup.config import ConfigManager @@ -41,6 +42,49 @@ def str2bool(v): raise argparse.ArgumentTypeError('Boolean value expected.') +def parse_period(period_str, date_format=None, delimiter=None): + """ + parse a period string (e.g. '2000-01-01_2001-01-01') into two datetimes. + requires a user-specified date_format (e.g. 'YYYY-MM-DD'). + + Parameters + ---------- + period_str : str + period string to parse + date_format : str, optional + the date format to use for parsing (default: None, i.e., try to infer automatically) + delimiter : str, optional + the delimiter to use for splitting the period string (default: None, i.e., try common delimiters) + Returns + ------- + t1, t2 : pd.Timestamp + the two parsed datetimes + """ + + if not date_format: + raise ValueError("Period date_format must be provided (e.g. 'YYYY-MM-DD').") + if not delimiter: + raise ValueError("Period delimiter must be provided (e.g. '_').") + + # split and validate + parts = [p.strip() for p in period_str.split(delimiter)] + if len(parts) != 2: + raise ValueError(f"Could not split '{period_str}' into two valid dates using '{delimiter}'.") + + # parse both parts + try: + t1 = pd.to_datetime(parts[0], format=date_format) + t2 = pd.to_datetime(parts[1], format=date_format) + except Exception as e: + raise ValueError(f"Failed to parse '{period_str}' with format '{date_format}'") from e + + # ensure t2 > t1 + if t2 <= t1: + raise ValueError(f"Invalid period '{period_str}': t2 ({t2.date()}) must be later than t1 ({t1.date()}).") + + return t1, t2 + + def annualweightedmean_array(var, dates_table): """ Calculate annual mean of variable according to the timestep. diff --git a/pyproject.toml b/pyproject.toml index dcb5a13c..5fce5249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pygem" -version = "1.0.4" +version = "1.1.0-beta" description = "Python Glacier Evolution Model (PyGEM)" authors = ["David Rounce ,Brandon Tober "] license = "MIT License" From ea2977fd343eb1d2169318775a023ac689d2f3c4 Mon Sep 17 00:00:00 2001 From: David Rounce Date: Wed, 5 Nov 2025 17:02:35 -0500 Subject: [PATCH 19/19] 161 mcmc daily mbmaxloss update (#162) Closes #161 * Removed some hard-coded monthly code Closes #160 * Add simple_test_daily to test_03_notebooks (#163) --------- Co-authored-by: Brandon S. Tober --- docs/install_pygem.md | 2 +- pygem/bin/run/run_calibration.py | 19 ++++++++++--------- pygem/tests/test_03_notebooks.py | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/install_pygem.md b/docs/install_pygem.md index 531a5b9a..6517b343 100644 --- a/docs/install_pygem.md +++ b/docs/install_pygem.md @@ -12,7 +12,7 @@ Next, choose your preferred PyGEM installation option:
(stable_install_target)= ## Stable install -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/master/docs/pygem_environment.yml). +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). From the folder where you saved the file, run `conda env create -f pygem_environment.yml`. ```{note} diff --git a/pygem/bin/run/run_calibration.py b/pygem/bin/run/run_calibration.py index e35fe76e..1899e272 100755 --- a/pygem/bin/run/run_calibration.py +++ b/pygem/bin/run/run_calibration.py @@ -307,8 +307,11 @@ def run_oggm_dynamics(gdir, modelprms, glacier_rgi_table, fls): / pygem_prms['constants']['density_water'] ) # record each year's frontal ablation in m3 w.e. - for n in np.arange(calving_m3we_annual.shape[0]): - ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3we_annual[n] + if pygem_prms['time']['timestep'] == 'monthly': + for n in np.arange(calving_m3we_annual.shape[0]): + ev_model.mb_model.glac_wide_frontalablation[12 * n + 11] = calving_m3we_annual[n] + else: + raise ValueError('Need to add functionality for daily timestep') # add mass lost from frontal ablation to Glacier-wide total mass balance (m3 w.e.) ev_model.mb_model.glac_wide_massbaltotal = ( @@ -831,15 +834,12 @@ def run(list_packed_vars): t1_month = gdir.mbdata['t1_datetime'].month t2_year = gdir.mbdata['t2_datetime'].year t2_month = gdir.mbdata['t2_datetime'].month - t1_idx = dates_table[ + gdir.mbdata['t1_idx'] = dates_table[ (t1_year == dates_table['year']) & (t1_month == dates_table['month']) ].index.values[0] - t2_idx = dates_table[ + gdir.mbdata['t2_idx'] = dates_table[ (t2_year == dates_table['year']) & (t2_month == dates_table['month']) - ].index.values[0] - # Record indices - gdir.mbdata['t1_idx'] = t1_idx - gdir.mbdata['t2_idx'] = t2_idx + ].index.values[-1] if debug: print( @@ -1995,7 +1995,8 @@ def rho_constraints(**kwargs): * consensus_mass / pygem_prms['constants']['density_water'] / gdir.rgi_area_m2 - / (gdir.dates_table.shape[0] / 12) + # / (gdir.dates_table.shape[0] / 12) + / gdir.mbdata['nyears'] ) # --------------------------------- diff --git a/pygem/tests/test_03_notebooks.py b/pygem/tests/test_03_notebooks.py index 6af257ba..0c372605 100644 --- a/pygem/tests/test_03_notebooks.py +++ b/pygem/tests/test_03_notebooks.py @@ -19,6 +19,7 @@ # list of notebooks to test, in the desired order (failures may occur if order is changed) notebooks = [ 'simple_test.ipynb', # runs with sample_data + 'simple_test_daily.ipynb', # runs with sample_data 'advanced_test.ipynb', # runs with sample_data 'dhdt_processing.ipynb', # runs with sample_data 'advanced_test_spinup_elev_change_calib.ipynb', # runs with sample_data, depends on dhdt_processing.ipynb results