diff --git a/.github/workflows/test_suite.yml b/.github/workflows/test_suite.yml index 4bb4a542..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" @@ -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/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/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/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/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/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/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/__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 """ @@ -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/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..ee96e737 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) """ @@ -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=''): @@ -88,9 +86,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 +95,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 +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 = '1Wu4ZqpOKxnc4EYhcRHQbwGq95FoOxMfZ' + file_id = '1cRVG__7dVclut42LdQBjnXKpTvyYWBuK' # 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 29e98b22..1fc0196e 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 """ @@ -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 deleted file mode 100644 index cf57db07..00000000 --- a/pygem/bin/postproc/postproc_binned_monthly_mass.py +++ /dev/null @@ -1,328 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2024 Brandon Tober David Rounce - -Distrubted under the MIT lisence - -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 0de32b39..2247687e 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 """ @@ -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': { @@ -189,9 +190,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]) @@ -221,7 +220,7 @@ 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] @@ -230,7 +229,7 @@ def run(args): base_dir + gcm + '/stats/' - + f'*{gcm}_{calibration}_ba{bias_adj}_*_{sim_startyear}_{sim_endyear}_all.nc' + + 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] @@ -238,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)) @@ -290,13 +295,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)}%)' @@ -326,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] @@ -417,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 @@ -432,20 +435,13 @@ 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['comment'] = 'start of the month' - + else: + ds.time.attrs['range'] = str(time_values[0]) + ' - ' + str(time_values[-1]) + 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['comment'] = 'RGIv6.0 (https://nsidc.org/data/nsidc-0770/versions/6)' ds.RGIId.attrs['cf_role'] = 'timeseries_id' if realizations[0]: @@ -504,9 +500,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: @@ -607,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', @@ -686,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_distribute_ice.py b/pygem/bin/postproc/postproc_distribute_ice.py index cfbab6fc..688ef89b 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 @@ -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_subannual_mass.py similarity index 50% rename from pygem/bin/postproc/postproc_monthly_mass.py rename to pygem/bin/postproc/postproc_subannual_mass.py index 267da544..7931d937 100644 --- a/pygem/bin/postproc/postproc_monthly_mass.py +++ b/pygem/bin/postproc/postproc_subannual_mass.py @@ -3,20 +3,24 @@ 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 +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,37 +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'] + + 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') + + # Overlay annual masses as points/line + plt.plot(df_annual['time'], df_annual['mass'], 'o--', label='Annual mass', color='orange', markersize=6) - # add annual mass values to running glacier mass balance - glac_mass_monthly += running_glac_massbaltotal_monthly + # 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() - return glac_mass_monthly + return df_sub['mass'].values -def update_xrdataset(input_ds, glac_mass_monthly): +def update_xrdataset(input_ds, glac_mass, timestep): """ update xarray dataset to add new fields @@ -128,29 +160,22 @@ 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 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], @@ -169,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 @@ -187,25 +211,33 @@ 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() # 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() @@ -237,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/preproc/preproc_fetch_mbdata.py b/pygem/bin/preproc/preproc_fetch_mbdata.py index 70d1f2b5..7a3af64b 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 """ @@ -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 841cdbf3..21bc8dd5 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 @@ -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/__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 """ @@ -16,6 +16,7 @@ import os import pickle import time +import warnings from datetime import timedelta import gpytorch @@ -37,21 +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.glacierdynamics import MassRedistributionCurveModel -from pygem.oggm_compat import ( - single_flowline_glacier_directory, - single_flowline_glacier_directory_with_calving, -) +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(): @@ -116,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', @@ -168,6 +163,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( @@ -176,8 +178,23 @@ def getparser(): help='Flag to keep glacier lists ordered (default is false)', ) parser.add_argument( - '-p', '--progress_bar', action='store_true', help='Flag to show progress bar' + '-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', + 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( + '-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 +230,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 +244,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 +252,203 @@ 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 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) + # 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. + 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 = ( + 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 + + return mbmod, ds + + +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 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(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 + + # --- 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_annual[:, 0][:, np.newaxis] + + # --- 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 + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + bin_thick_subannual = np.column_stack( + [ + stats.binned_statistic( + x=ref_surface_height, + values=x, + statistic=np.nanmean, + bins=gdir.elev_change_1d['bin_edges'], + )[0] + for x in bin_thick_subannual.T + ] + ) + # interpolate over any empty bins + bin_thick_subannual = np.column_stack([interp1d_fill_gaps(x.copy()) for x in bin_thick_subannual.T]) + + # --- 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 + else np.full(bin_thick_subannual.shape[0], np.nan) + for tup in gdir.elev_change_1d['model2obs_inds_map'] + ] + ) + + 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 class ExactGPModel(gpytorch.models.ExactGP): """Use the simplest form of GP model, exact inference""" @@ -277,9 +477,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 +497,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 +579,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 +602,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 +610,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 +643,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 +655,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 +727,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,31 +745,32 @@ 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( - 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( - gcm.elev_fn, gcm.elev_vn, main_glac_rgi - ) - # Lapse rate [degC m-1] + 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, 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, ) # ===== LOOP THROUGH GLACIERS TO RUN CALIBRATION ===== @@ -601,15 +788,13 @@ 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'] - ): - gdir = single_flowline_glacier_directory(glacier_str) + # 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 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') @@ -627,10 +812,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: @@ -647,24 +829,17 @@ 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']) + gdir.mbdata['t1_idx'] = dates_table[ + (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']) - ].index.values[0] - # Record indices - gdir.mbdata['t1_idx'] = t1_idx - gdir.mbdata['t2_idx'] = t2_idx + gdir.mbdata['t2_idx'] = dates_table[ + (t2_year == dates_table['year']) & (t2_month == dates_table['month']) + ].index.values[-1] if debug: print( @@ -673,45 +848,87 @@ 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'] + ] + # 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) - + '/' - ) + 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 debug: - assert os.path.exists(mbdata_fn), ( - 'Mass balance data missing. Check dataset and column names' - ) - # ----- CALIBRATION OPTIONS ------ if (fls is not None) and (gdir.mbdata is not None) and (glacier_area.sum() > 0): modelprms = { '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'], } @@ -728,34 +945,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 @@ -835,8 +1040,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, @@ -944,23 +1148,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, @@ -969,15 +1165,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: @@ -1012,10 +1205,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, @@ -1068,19 +1258,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) @@ -1091,9 +1274,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( @@ -1150,13 +1331,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( @@ -1201,9 +1377,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] @@ -1223,32 +1397,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( @@ -1274,18 +1433,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) @@ -1343,16 +1498,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:', @@ -1376,9 +1527,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 ' @@ -1402,9 +1551,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 ' @@ -1423,9 +1570,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') @@ -1554,14 +1699,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: @@ -1669,10 +1810,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) @@ -1696,10 +1834,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}' @@ -1707,9 +1842,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 ----- @@ -1728,19 +1861,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 @@ -1751,12 +1874,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 @@ -1766,9 +1884,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.] @@ -1778,65 +1894,67 @@ 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( - low=priors[param]['low'], high=priors[param]['high'] - ) - 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): - # 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)] + 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] - # Check if all densities are greater than the threshold - if all(p > threshold for p in ps): - initials = xs - return initials + # default: random sampling within central probability interval + lower = (1 - central_mass) / 2 + upper = 1 - lower - def mb_max(*args, **kwargs): - """Model parameters cannot completely melt the glacier (psuedo-likelihood fxn)""" + 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.""" 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: @@ -1844,6 +1962,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 + # --------------------------------- # --------------------------------- @@ -1858,10 +1987,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 = ( @@ -1869,7 +1995,8 @@ def must_melt(kp, tbias, ddfsnow, **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'] ) # --------------------------------- @@ -1880,9 +2007,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']) @@ -1896,9 +2021,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'] @@ -1910,30 +2033,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) # ------------------ # ----------------------------------- @@ -1942,12 +2055,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 + '-' @@ -1963,40 +2071,89 @@ def must_melt(kp, tbias, ddfsnow, **kwargs): # ------------------- # --- set up MCMC --- # ------------------- - # mass balance observation and standard deviation - obs = [(torch.tensor([mb_obs_mwea]), torch.tensor([mb_obs_mwea_err]))] - # 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))) + # 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) - mbargs = None # no additional arguments for mbEmulator.eval() - else: - mbfxn = mb_mwea_calc # returns (mb_mwea) - mbargs = ( + + # 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']), + } + + # model equilibrium line elevation for breakpoint of accumulation and ablation area density scaling + gdir.ela = tasks.compute_ela( gdir, - modelprms, - glacier_rgi_table, - fls, - ) # arguments for mb_mwea_calc() + years=np.arange( + gdir.dates_table.year.min(), + min(2019, gdir.dates_table.year.max() + 1), + ), + ) + + # 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']), + ) + # 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) - 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], + 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, ) - # 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['elev_change_1d']] + 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] = {} # ------------------- @@ -2005,76 +2162,83 @@ 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 - - # 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 - ) + # 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 - # 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 not chain_completed and debug: + print( + f'Chain {n_chain}: failed to produce an unstuck result after {attempts_per_chain} initial guesses.' + ) 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), + 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:', @@ -2087,28 +2251,46 @@ 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, + pred_primes, + pred_chain, + obs, + 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 k in pred_chain.keys(): + graphics.plot_resid_histogram( + obs[k], + pred_chain[k], + glacier_str, + show=show, + fpath=f'{fp}/{glacier_str}-chain{n_chain}-residuals-{k}.png', + ) + if k == 'elev_change_1d': + graphics.plot_mcmc_elev_change_1d( + pred_chain[k], + 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) @@ -2116,38 +2298,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['elev_change_1d'] + ] + 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( @@ -2180,9 +2353,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 @@ -2196,9 +2367,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 ===== @@ -2208,24 +2377,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 ----- @@ -2273,13 +2433,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( @@ -2320,9 +2475,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] @@ -2342,32 +2495,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( @@ -2398,9 +2536,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) ( @@ -2441,14 +2577,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: @@ -2478,9 +2610,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: @@ -2488,22 +2618,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 @@ -2530,10 +2650,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:', @@ -2556,8 +2673,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 @@ -2575,9 +2691,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:', @@ -2632,12 +2746,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 @@ -2666,10 +2775,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): @@ -2715,9 +2821,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}, ) @@ -2739,8 +2843,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 @@ -2817,9 +2920,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:', @@ -2853,9 +2954,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:', @@ -2928,12 +3027,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 @@ -2948,12 +3042,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' @@ -2999,9 +3088,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 @@ -3010,7 +3097,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 9523174b..fe53157f 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 """ @@ -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,31 +128,27 @@ 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( - 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( - 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 + gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table, verbose=debug ) # ===== CALIBRATE ALL THE GLACIERS AT ONCE ===== @@ -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'], } @@ -260,34 +244,22 @@ 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['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 @@ -945,16 +873,14 @@ 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) - fa_glac_data['O1Region'] = [ - int(x.split('-')[1].split('.')[0]) for x in fa_glac_data.RGIId.values - ] + 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) 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) @@ -2782,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, @@ -2794,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 @@ -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( @@ -2832,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 = [] @@ -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) @@ -3074,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', @@ -3116,13 +2743,13 @@ 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}' + 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 @@ -3139,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) @@ -3157,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/bin/run/run_calibration_reg_glena.py b/pygem/bin/run/run_calibration_reg_glena.py deleted file mode 100644 index e73963b9..00000000 --- a/pygem/bin/run/run_calibration_reg_glena.py +++ /dev/null @@ -1,589 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2018 David Rounce - -Distrubted under the MIT lisence - -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 in ['ERA5', 'ERA-Interim'], ( - '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 - ) - 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 - ) - 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 - ) - # 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 - ) - - # ===== 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"]["glena_reg_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"]["glena_reg_relpath"]}', - index=False, - ) - - print('\n\n------\nTotal processing time:', time.time() - time_start, 's') - - -if __name__ == '__main__': - main() diff --git a/pygem/bin/run/run_inversion.py b/pygem/bin/run/run_inversion.py index d118ad5a..24ffaa2b 100644 --- a/pygem/bin/run/run_inversion.py +++ b/pygem/bin/run/run_inversion.py @@ -1,10 +1,13 @@ import argparse +import json import os from functools import partial import numpy as np import pandas as pd +pd.set_option('display.float_format', '{:.3e}'.format) + # pygem imports from pygem.setup.config import ConfigManager @@ -12,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 @@ -21,14 +25,91 @@ # 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(logging_level=pygem_prms['oggm']['logging_level']) +cfg.PATHS['working_dir'] = f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' + + +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 'O1Region' + if os.path.exists(outpath): + existing_df = pd.read_csv(outpath) + # remove rows with the same 'O1Region' values as in the new merge + merged_df = pd.concat( + [existing_df[~existing_df['O1Region'].isin(merged_df['O1Region'])], merged_df], ignore_index=True + ) + # re-sort + merged_df = merged_df.sort_values('O1Region') + + # export final merged csv + merged_df.to_csv(outpath, index=False) -cfg.initialize() -cfg.PATHS['working_dir'] = ( - f'{pygem_prms["root"]}/{pygem_prms["oggm"]["oggm_gdir_relpath"]}' -) + # Delete individual regional files + for fp in filepaths_to_delete: + os.remove(fp) -def run(glac_no, ncores=1, debug=False): +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. """ @@ -38,18 +119,23 @@ def run(glac_no, ncores=1, debug=False): 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 @@ -61,27 +147,21 @@ def run(glac_no, ncores=1, debug=False): # 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( - 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 + 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): @@ -134,12 +214,11 @@ 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 + # apply inversion_filter on mass balance with debris to avoid negative flux workflow.execute_entity_task( tasks.apparent_mb_from_any_mb, gdirs, @@ -147,81 +226,112 @@ 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 - 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 ### ########################## # 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") + 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 + 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 + ) + itmix_vol = cdf.sum()['vol_itmix_m3'] + model_vol = cdf.sum()['vol_oggm_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 ( - 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 + if calibrate_regional_glen_a: + glen_a = gdir.get_diagnostics()['inversion_glen_a'] + fs = gdir.get_diagnostics()['inversion_fs'] 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 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 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 ### @@ -232,11 +342,40 @@ def run(glac_no, ncores=1, debug=False): # 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( + { + 'O1Region': 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( @@ -246,35 +385,109 @@ def main(): help='Randoph Glacier Inventory region (can take multiple, e.g. `-run_region01 1 2 3`)', nargs='+', ) + parser.add_argument( + '-rgi_glac_number', + 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', + 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( + '-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', type=int, default=1, - help='number of simultaneous processes (cores) to use', + 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', + 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 - ] + # --- 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 = [ + 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 + ] + regional_inv = True # flag to regional inversion + 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] + 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) # 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, + 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() 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 3066befe..3a3bc0fd 100755 --- a/pygem/bin/run/run_simulation.py +++ b/pygem/bin/run/run_simulation.py @@ -3,13 +3,13 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license 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. @@ -17,7 +17,6 @@ # Built-in libraries import argparse import copy -import inspect import json import multiprocessing import os @@ -41,8 +40,8 @@ # read the config pygem_prms = config_manager.read_config() # oggm imports -from oggm import cfg, graphics, tasks, utils -from oggm.core.flowline import FluxBasedModel +from oggm import cfg, tasks, utils +from oggm.core.flowline import FluxBasedModel, SemiImplicitModel from oggm.core.massbalance import apparent_mb_from_any_mb import pygem.gcmbiasadj as gcmbiasadj @@ -55,6 +54,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 @@ -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 @@ -359,9 +365,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 @@ -401,17 +405,19 @@ 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', '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, @@ -424,28 +430,24 @@ 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: - 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 @@ -474,17 +476,16 @@ 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: # Temperature bias correction @@ -498,14 +499,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: @@ -541,42 +540,50 @@ 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 + 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, 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( - 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']: - gcm_lr, gcm_dates = gcm.importGCMvarnearestneighbor_xarray( - gcm.lr_fn, gcm.lr_vn, main_glac_rgi, dates_table - ) - 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 - ) - # 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 @@ -586,11 +593,8 @@ def run(list_packed_vars): 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' + 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: @@ -606,22 +610,13 @@ 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 - ) + 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 @@ -647,10 +642,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, ) @@ -664,15 +659,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) @@ -686,12 +676,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'], } @@ -703,28 +689,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: @@ -739,9 +710,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) @@ -749,46 +718,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( @@ -804,18 +762,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: @@ -828,110 +779,69 @@ 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']) - 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, ( - 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 + 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': - 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_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_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_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 @@ -969,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 @@ -978,65 +887,57 @@ 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 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( + 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 @@ -1062,243 +963,94 @@ 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!') - # 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) - plt.show() - - 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] + fig, ax = plt.subplots(1) + graphics.plot_modeloutput_section( + ev_model, ax=ax, lnlabel=f'Glacier year {args.sim_startyear}' ) - 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] - - # 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 - ) - 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, - ), - ) + 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] - 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 [ - 'ERA-Interim', - '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, - ) - _, 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 + if debug: + print( + 'avg calving_m3:', + calving_m3_annual.sum() / nyears, ) - - # 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!') @@ -1309,92 +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: - print('New glacier vol', ev_model.volume_m3) - graphics.plot_modeloutput_section(ev_model) - plt.show() - 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 + fig, ax = plt.subplots(1) + graphics.plot_modeloutput_section( + ev_model, ax=ax, lnlabel=f'Glacier year {args.sim_startyear}' ) - # 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 - ) + _, 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 - 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, - ), - ) + # 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 [ - 'ERA-Interim', - '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 @@ -1410,29 +1124,22 @@ 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] - ) + 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 @@ -1448,16 +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) - # graphics.plot_modeloutput_map(gdir, model=ev_model) + 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() @@ -1472,16 +1176,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), @@ -1507,62 +1205,43 @@ 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'] + 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_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] - ) + 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) @@ -1576,89 +1255,49 @@ 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 - ] - ) - # 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_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_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 - 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) @@ -1671,19 +1310,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, @@ -1700,62 +1333,42 @@ 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 = 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, ) @@ -1768,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, @@ -1779,95 +1393,67 @@ 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 - } - ) + 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'].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'].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_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'].values[0, :] = ( + output_glac_temp_steps[:, n_iter] + 273.15 + ) + 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_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_snowline'].values[0, :] = output_glac_snowline_steps[ + :, 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['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['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['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['offglac_snowpack'].values[0, :] = output_offglac_snowpack_steps[ :, 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.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, @@ -1879,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 @@ -1886,203 +1473,107 @@ 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_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_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_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'].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'].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_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_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['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['offglac_snowpack_monthly'].values[0, :] = ( - output_offglac_snowpack_monthly_stats[:, 0] + 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'].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_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_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['glac_ELA_annual_mad'].values[0, :] = output_glac_ELA_annual_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[:, 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_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_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_massbaltotal_mad'].values[0, :] = ( + output_glac_massbaltotal_steps_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_snowline_mad'].values[0, :] = output_glac_snowline_steps_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_mass_change_ignored_annual_mad'].values[0, :] = ( + output_glac_mass_change_ignored_annual_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_snowpack_mad'].values[0, :] = ( + output_offglac_snowpack_steps_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 @@ -2092,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, @@ -2105,64 +1597,49 @@ 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 - } - ) + 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'].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[ + output_ds_binned_stats['bin_accumulation'].values[0, :, :] = ( + output_glac_bin_acc_steps[:, :, n_iter] + ) + output_ds_binned_stats['bin_melt'].values[0, :, :] = output_glac_bin_melt_steps[ :, :, 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_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() @@ -2170,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, @@ -2183,18 +1661,15 @@ 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() 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, :, :] @@ -2204,68 +1679,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_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)[ - np.newaxis, :, : - ] - output_ds_binned_stats[ - 'bin_melt_monthly' - ].values = np.median(output_glac_bin_melt_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'].values = np.median(output_glac_bin_melt_steps, 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_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( + 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) @@ -2273,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(): @@ -2322,9 +1767,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 @@ -2365,14 +1808,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 2ae7eb24..a3638e96 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,57 +31,168 @@ single_flowline_glacier_directory_with_calving, update_cfg, ) +from pygem.utils._funcs import interp1d_fill_gaps -def run(glacno_list, spinup_start_yr, **kwargs): +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: + 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, + ) + + ### 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_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') + bin_thick_monthly = np.column_stack( + [ + binned_statistic( + x=ref_surface_height, + values=x, + statistic=np.nanmean, + bins=gdir.elev_change_1d['bin_edges'], + )[0] + for x in bin_thick_monthly.T + ] + ) + # interpolate over any empty bins + bin_thick_monthly = np.column_stack([interp1d_fill_gaps(x.copy()) for x in bin_thick_monthly.T]) + + # --- Step 6: calculate elevation change --- + elev_change_1d = np.column_stack( + [ + 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 elev_change_1d, 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=spinup_start_yr, endyear=2019 - ) # will have to cover the time period of inversion (2000-2019) and spinup (1979-~2010 by default) - # 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] 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( - 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 + 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 @@ -91,48 +208,234 @@ 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'], - } + # 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( + (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') 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=, # 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'] + # 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 + + # 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 + + # 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 = calculate_elev_change_1d(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 + # 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 - 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] + + ############################ + ### 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 @@ -169,21 +472,61 @@ 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('-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', 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='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: @@ -206,9 +549,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: @@ -221,8 +562,18 @@ 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, + 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 e39e19dd..de069ee4 100755 --- a/pygem/class_climate.py +++ b/pygem/class_climate.py @@ -3,12 +3,13 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Run bias adjustments a given climate dataset """ @@ -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 5ba4b8ad..16f2e910 100755 --- a/pygem/glacierdynamics.py +++ b/pygem/glacierdynamics.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce 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 +167,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 +241,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 +297,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? @@ -346,8 +323,13 @@ def run_until_and_store( 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): @@ -358,12 +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 @@ -373,9 +351,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 +360,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 +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'] @@ -424,14 +396,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, t_stop + 1] = ( fa_m3 * pygem_prms['constants']['density_ice'] / pygem_prms['constants']['density_water'] @@ -457,9 +425,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,13 +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) @@ -507,23 +468,16 @@ 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 - ): + 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 +498,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 +514,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 +546,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 +606,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 +624,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 +654,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 +662,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 +699,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 +715,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 +760,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 +802,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 +812,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 +835,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 +860,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 +909,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 +981,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 +998,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 +1028,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 c19096c4..20425eb7 100644 --- a/pygem/massbalance.py +++ b/pygem/massbalance.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce - 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] @@ -257,9 +265,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 @@ -274,724 +280,505 @@ 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: 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) 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 - ) + 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 - # 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 - ) - + 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] - + self.modelprms['tbias'] - ) + # 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[:, 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] + # 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), + ) + 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'] + ] + ) = 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] - ) + ) = 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' + ) - # 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 + # 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, ) - # 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 + # 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 ] - # 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] ) - # 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] + 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' ) - # 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'] - ) + 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', + ) - 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 - 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 + # 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, ): - print( - '\nMonth ' - + str(self.dates_table.loc[step, 'month']), - 'Computing heat conduction', + # 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 + else: + if self.debug_refreeze and gidx == gidx_debug and step < 12: + print( + '\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 + # 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'] ) - # 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, + / 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'] ) - # 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' - ) + self.rf_cold[gidx] -= rf_cold_layer - 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 - - 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 - ): + if self.debug_refreeze and gidx == gidx_debug and step < 12: print( - 'rf_cold:', np.round(self.rf_cold[gidx], 2) + '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), ) - # 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)) - # Record refreeze - self.bin_refreeze[gidx, step] = self.refr[gidx] + # 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 - 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), - ) + # 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 - 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] + # Record refreeze + self.bin_refreeze[gidx, step] = self.refr[gidx] - 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] + 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 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] + + # 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] ) - self.snowpack_remaining[ - abs(self.snowpack_remaining[:, step]) - < pygem_prms['constants']['tolerance'], + # 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 @@ -1009,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]) @@ -1021,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, @@ -1048,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 @@ -1065,10 +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 @@ -1084,111 +868,82 @@ 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.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 = 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) @@ -1199,48 +954,36 @@ 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_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): @@ -1257,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] @@ -1270,43 +1029,31 @@ 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] 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.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 - ) + 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 +1089,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 +1115,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 +1159,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 +1218,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 f628672f..b10ad61c 100644 --- a/pygem/mcmc.py +++ b/pygem/mcmc.py @@ -1,9 +1,9 @@ """ Python Glacier Evolution Model (PyGEM) -copyright © 2018 David Rounce , David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license Markov chain Monte Carlo methods """ @@ -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,28 +120,54 @@ 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 + 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() + 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,30 +191,24 @@ 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.fxnargs[1][k] = float(m[i]) + self.fxnargs[1]['ddfice'] = self.fxnargs[1]['ddfsnow'] / pygem_prms['sim']['params']['ddfsnow_iceratio'] - # get mb_pred - def get_mb_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 + # get model predictions + def get_model_pred(self, m): + 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): @@ -202,28 +222,62 @@ 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): + 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 k == 'glacierwide_mb_mwea' and not self.calib_glacierwide_mb_mwea: + continue # skip this model output if not calibrating glacierwide mass balance + + # 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) + # 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[i][0], **{'mu': pred, 'sigma': self.obs[i][1]} + self.obs[k][0], # observations + mu=pred, # scaled predictions + sigma=self.obs[k][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['glacierwide_mb_mwea'], + } + + # --- 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 +304,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: @@ -272,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( @@ -289,7 +345,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 +358,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,15 +389,13 @@ 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)) - ) - 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]) + self.acceptance.append(self.naccept / (i + (thin_factor * self.n_rm))) + 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): @@ -370,220 +425,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 5e303e99..264feb56 100755 --- a/pygem/oggm_compat.py +++ b/pygem/oggm_compat.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce 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_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( @@ -460,10 +438,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,105 +456,97 @@ 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_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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('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', @@ -586,145 +554,123 @@ 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('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,47 +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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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( - [('glac', self.glac_values), ('time', self.time_values)] - ) + 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', } @@ -891,62 +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( - [ - ('glac', self.glac_values), - ('bin', self.bin_values), - ('time', self.time_values), - ] - ) + 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 @@ -977,14 +913,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 +959,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..7194dc6a --- /dev/null +++ b/pygem/plot/graphics.py @@ -0,0 +1,557 @@ +""" +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 +import torch +from scipy.stats import binned_statistic + +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, + 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() + """ + + 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() + # get n lines plotted on figure + nlines = len(plt.gca().get_lines()) + + height = np.array([]) + bed = np.array([]) + for cls in fls: + 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 + + 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] + 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=lnlabel) + + # 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)', + ) + + 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') + + +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 + 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() + + # get n_eff + neff = [effective_n(arr) for arr in m_chain.T] + # instantiate list to hold legend objs + legs = [] + + # axes[0] will always be tbias + 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] 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( + [], + [], + 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] 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( + [], + [], + 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 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') + 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) + + # 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') + 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) + + # 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'), + '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') + 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() + 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, + ) + 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) + 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 905e00ec..18a7bb33 100755 --- a/pygem/pygem_modelsetup.py +++ b/pygem/pygem_modelsetup.py @@ -3,7 +3,7 @@ copyright © 2018 David Rounce = 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 = {} @@ -141,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. @@ -197,8 +145,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 +189,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 +298,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 +340,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 +353,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 +374,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 +393,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 +405,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 +439,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 +454,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 +534,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/__init__.py b/pygem/setup/__init__.py index a4fc2db0..2c862bce 100755 --- a/pygem/setup/__init__.py +++ b/pygem/setup/__init__.py @@ -3,5 +3,5 @@ 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..0666329d 100644 --- a/pygem/setup/config.py +++ b/pygem/setup/config.py @@ -3,9 +3,10 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +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,14 +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.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): @@ -99,6 +100,20 @@ 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*', + ] + + # --- Type validation (existing code) --- for key, expected_type in self.EXPECTED_TYPES.items(): keys = key.split('.') sub_data = config @@ -109,11 +124,8 @@ 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): elem_type = self.LIST_ELEMENT_TYPES[key] if not all(isinstance(item, elem_type) for item in sub_data): @@ -121,6 +133,46 @@ def _validate_config(self, config): f"Invalid type for elements in '{key}': expected all elements to be {elem_type}, but got {sub_data}" ) + 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 + + 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 = { 'root': str, @@ -129,11 +181,11 @@ 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, + 'setup.min_glac_area_km2': (int, float), 'setup.include_landterm': bool, 'setup.include_laketerm': bool, 'setup.include_tidewater': bool, @@ -157,7 +209,7 @@ def _validate_config(self, config): 'climate.sim_wateryear': str, 'climate.constantarea_years': int, 'climate.paths': dict, - '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, @@ -178,21 +230,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, @@ -201,67 +253,91 @@ 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, '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': 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), + '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, - '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.icethickness_cal_frac_byarea': float, + '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, + 'calib.data.snowline_1d.snowline_1d_relpath': (str, type(None)), 'sim': dict, 'sim.option_dynamics': (str, type(None)), 'sim.option_bias_adjustment': int, @@ -272,26 +348,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.glena_reg_relpath': str, - 'sim.oggm_dynamics.use_reg_glena': bool, - 'sim.oggm_dynamics.fs': int, - 'sim.oggm_dynamics.glen_a_multiplier': int, - 'sim.icethickness_advancethreshold': int, - 'sim.terminus_percentage': int, + 'sim.oggm_dynamics.glen_a_regional_relpath': str, + 'sim.oggm_dynamics.use_regional_glen_a': bool, + '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, @@ -308,12 +384,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, @@ -333,13 +409,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 57b6ae27..97e838cf 100644 --- a/pygem/setup/config.yaml +++ b/pygem/setup/config.yaml @@ -73,6 +73,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/ @@ -149,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 @@ -182,25 +184,52 @@ 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 + # 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 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/ - - 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. + h_ref_relpath: /IceThickness_Farinotti/composite_thickness_RGI60-all_regions/ + # 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) + # 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) # ===== SIMULATION ===== sim: @@ -218,7 +247,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 @@ -226,8 +255,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 @@ -324,7 +353,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: diff --git a/pygem/shop/debris.py b/pygem/shop/debris.py index 0c4c922b..247017a8 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 @@ -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..6d1abc96 --- /dev/null +++ b/pygem/shop/elevchange1d.py @@ -0,0 +1,430 @@ +""" +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 +from scipy.stats import binned_statistic + +# 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 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. + + 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 + 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'{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'): + 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) + + # 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') + + +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) + + # 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() + + # Handle bin_area if present + if 'bin_area' in df.columns: + bin_area = df_unique_bins['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 + + +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/icethickness.py b/pygem/shop/icethickness.py index 256bf526..9d5dd314 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 @@ -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 b62dc856..944b8ea7 100755 --- a/pygem/shop/mbdata.py +++ b/pygem/shop/mbdata.py @@ -3,11 +3,10 @@ copyright © 2018 David Rounce -Distrubted under the MIT lisence +Distributed under the MIT license """ # Built-in libaries -import json import logging import os @@ -16,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() @@ -48,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 @@ -58,30 +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}' - ) - 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' - mb_clim_cn = 'mb_clim_mwea' - mberr_clim_cn = 'mb_clim_mwea_err' + assert os.path.exists(mbdata_fp), 'Error, mass balance dataset does not exist: {mbdata_fp}' + + # 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) @@ -95,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) @@ -117,21 +119,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, - 't1_str': t1_str, - 't2_str': t2_str, + '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_datetime.strftime('%Y-%m-%d'), + 't2_str': t2_datetime.strftime('%Y-%m-%d'), '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 new file mode 100644 index 00000000..5d4f418b --- /dev/null +++ b/pygem/shop/meltextent_and_snowline_1d.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 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': 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 + 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', + '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 + 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: + 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__}).") + + 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 = data['ref_dem'].astype(str).tolist()[0] + 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': ref_dem, + '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': 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 + 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', + '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 + 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: + 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__}).") + + 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 = data['ref_dem'].astype(str).tolist()[0] + 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': ref_dem, + 'ref_dem_year': ref_dem_year, + } + return data_dict diff --git a/pygem/shop/oib.py b/pygem/shop/oib.py deleted file mode 100644 index b7be9611..00000000 --- a/pygem/shop/oib.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -Python Glacier Evolution Model (PyGEM) - -copyright © 2024 Brandon Tober , David Rounce - -Distrubted under the MIT lisence - -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 +105,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] @@ -110,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 1eb74738..79ecc5f1 100755 --- a/pygem/utils/_funcs.py +++ b/pygem/utils/_funcs.py @@ -3,14 +3,17 @@ 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 +import pandas as pd +from scipy.interpolate import interp1d from pygem.setup.config import ConfigManager @@ -20,6 +23,68 @@ 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 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. @@ -31,26 +96,21 @@ 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) 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) @@ -60,11 +120,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 @@ -93,15 +152,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/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 """ diff --git a/pyproject.toml b/pyproject.toml index dc276675..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" @@ -45,13 +45,12 @@ 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" 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" @@ -63,7 +62,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" @@ -81,6 +80,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 +97,4 @@ ignore = [ [tool.coverage.report] omit = ["pygem/tests/*"] show_missing = true -skip_empty = true +skip_empty = true \ No newline at end of file