"""Command line arguments to the oggm_prepro command
Type `$ oggm_prepro -h` for help
"""
# Standard libraries
import os
import sys
import shutil
import argparse
import time
import logging
import json
import importlib
from pathlib import Path
# External modules
import pandas as pd
import numpy as np
import geopandas as gpd
# Locals
import oggm.cfg as cfg
from oggm import utils, workflow, tasks, GlacierDirectory
from oggm.core import gis
from oggm.core.massbalance import MonthlyTIModel, SfcTypeTIModel
from oggm.exceptions import InvalidParamsError, InvalidDEMError, InvalidWorkflowError
# Module logger
from oggm.utils import get_prepro_base_url, file_downloader
log = logging.getLogger(__name__)
# The preprocessing levels. Besides the usual 0 to 5, two "half levels" allow
# to split L3 and L4 where the work stops being per-glacier and starts needing
# the whole RGI region at once. This is what makes it possible to run the
# expensive part in many small chunk jobs on a cluster:
#
# 2 -> 3a climate, mass balance calibration, apparent mb (chunkable)
# 3a -> 3 Glen A calibration, inversion, L3 summaries (whole region)
# 3 -> 4a the historical and dynamic spinup runs (chunkable)
# 4a -> 5 L4 summaries, then the L5 directories (whole region)
#
# `3a` holds different glacier directory content than `3`, so it is written to
# its own `L3a` folder (a scratch one, it can be deleted afterwards). `4a` on
# the other hand holds exactly the L4 directories - only the summary files are
# missing - so it writes straight into `L4`. The rule is: the folder is named
# after the glacier directory content.
PREPRO_LEVELS = ['0', '1', '2', '3a', '3', '4a', '4', '5']
# The half levels, as (integer level, name of the flag they set)
_HALF_LEVELS = {'3a': 3, '4a': 4}
def _parse_max_level(max_level):
"""Split `max_level` into an int and the two "stop early" flags."""
max_level = str(max_level)
if max_level not in PREPRO_LEVELS[1:]:
raise InvalidParamsError('max_level should be one of {}'
''.format(PREPRO_LEVELS[1:]))
# '3a' stops before the inversion, '4a' before the L4 summaries
if max_level in _HALF_LEVELS:
return _HALF_LEVELS[max_level], max_level == '3a', max_level == '4a'
return int(max_level), False, False
def _parse_start_level(start_level):
"""Split `start_level` into an int and the two "resume here" flags.
The integer is the level the *previous* full level block ends at, so that
all the existing `start_level <= n` logic keeps working untouched: a run
resuming at `3a` still has to enter the L3 block, hence start_level 2.
"""
if start_level is None:
return 0, False, False
start_level = str(start_level)
if start_level not in PREPRO_LEVELS[:-1]:
raise InvalidParamsError('start_level should be one of {}'
''.format(PREPRO_LEVELS[:-1]))
if start_level in _HALF_LEVELS:
return _HALF_LEVELS[start_level] - 1, start_level == '3a', start_level == '4a'
return int(start_level), False, False
def _level_dir_name(level):
"""The folder name holding the glacier directories of a level.
Almost always `L<level>`, but `4a` holds exactly the L4 directories - only
the summary files are missing - so it shares the `L4` folder instead of
making a copy of the largest directories of the workflow. `3a` does hold
something else than `L3` and gets its own folder.
"""
return 'L4' if str(level) == '4a' else f'L{level}'
def _level_dir(root, rgi_version, border, level):
"""The folder holding the glacier directories of a level.
Mirrors the layout that :py:func:`oggm.utils.get_prepro_base_url` builds
for the remote ones.
"""
return (Path(root) / f'RGI{rgi_version}' / f'b_{border:03d}' /
_level_dir_name(level))
def _forward_summary_path(basename, level, start_from_dir, start_base_url,
rgi_version, border):
"""Where to read a summary file which is only carried forward.
A run starting from glacier directories it did not make itself still has
to copy the summary files of the level it started from along. Those can
sit next to the directories on disk, or on the base url they were
downloaded from. A chunked run often has both at once: the directories
are local, because the previous stage wrote them, while the summary files
are still remote, because no local run ever made them.
"""
if start_from_dir is not None:
ipath = (_level_dir(start_from_dir, rgi_version, border, level) /
'summary' / basename)
if ipath.exists():
return ipath
if start_base_url is None:
raise InvalidWorkflowError(
f'Could not find {ipath}. This run has to carry the L{level} '
'summary files forward, but they are not next to the glacier '
'directories it started from - which is normal if a previous '
'stage made those directories without writing any summary. '
'Also point start_base_url at the url they came from.')
return file_downloader(os.path.join(
get_prepro_base_url(base_url=start_base_url, rgi_version=rgi_version,
border=border, prepro_level=int(level)),
'summary', basename))
def apply_rgi_fixes(rgidf, rgi_version, rgi_reg):
"""The RGI input quality fixes the preprocessing applies before running.
These are based on visual checks of large glaciers in the RGI. Note that
for Greenland this also *removes* glaciers, which is why the chunk
definition has to be computed after this has been applied - see
:py:func:`oggm.workflow.get_rgi_chunk`.
Parameters
----------
rgidf : geopandas.GeoDataFrame
the RGI region file
rgi_version : str
the RGI version it comes from
rgi_reg : str
the RGI region, zero padded to two digits
Returns
-------
the fixed dataframe
"""
if rgi_version != '62':
return rgidf
ids_to_ice_cap = [
'RGI60-05.10315', # huge Greenland ice cap
'RGI60-03.01466', # strange thing next to Devon
'RGI60-09.00918', # Academy of sciences Ice cap
'RGI60-09.00969',
'RGI60-09.00958',
'RGI60-09.00957',
]
rgidf.loc[rgidf.RGIId.isin(ids_to_ice_cap), 'Form'] = 1
# In AA almost all large ice bodies are actually ice caps
if rgi_reg == '19':
rgidf.loc[rgidf.Area > 100, 'Form'] = 1
# For greenland we omit connectivity level 2
if rgi_reg == '05':
rgidf = rgidf.loc[rgidf['Connect'] != 2]
return rgidf
@utils.entity_task(log)
def _rename_dem_folder(gdir, source=''):
"""Put the DEM files in a subfolder of the gdir.
Parameters
----------
gdir : GlacierDirectory
source : str
the DEM source
"""
# open tif-file to check if it's worth it
dem_f = gdir.get_filepath('dem')
try:
dem = gis.read_geotiff_dem(gdir)
except IOError:
# Error reading file, no problem - still, delete the file if needed
if os.path.exists(dem_f):
os.remove(dem_f)
gdir.log('{},DEM SOURCE,{}'.format(gdir.rgi_id, source),
err=InvalidDEMError('File does not exist'))
return
# Check the DEM
isfinite = np.isfinite(dem)
if np.all(~isfinite) or (np.min(dem) == np.max(dem)):
# Remove the file and return
if os.path.exists(dem_f):
os.remove(dem_f)
gdir.log('{},DEM SOURCE,{}'.format(gdir.rgi_id, source),
err=InvalidDEMError('DEM does not contain more than one '
'valid values.'))
return
# Create a source dir and move the files
out = os.path.join(gdir.dir, source)
utils.mkdir(out)
for fname in ['dem', 'dem_source']:
f = gdir.get_filepath(fname)
os.rename(f, os.path.join(out, os.path.basename(f)))
# log SUCCESS for this DEM source
gdir.log('{},DEM SOURCE,{}'.format(gdir.rgi_id, source))
@utils.entity_task(log)
def _move_hypsometry_to_dem_folder(gdir, source=''):
"""Move the hypsometry file to the DEM source folder if it exists.
Parameters
----------
gdir : GlacierDirectory
source : str
the DEM source
"""
hypso_f = gdir.get_filepath('hypsometry')
if not os.path.exists(hypso_f):
return
out = os.path.join(gdir.dir, source)
if not os.path.exists(out):
raise InvalidWorkflowError('We should not be there')
os.rename(hypso_f, os.path.join(out, os.path.basename(hypso_f)))
[docs]
def run_prepro_levels(rgi_version=None, rgi_reg=None, border=None,
output_folder='', working_dir='', dem_source='',
is_test=False, test_ids=None, rgi_file=None,
intersects_file=None, test_topofile=None,
disable_mp=False, params_file=None,
elev_bands=False, centerlines=False,
override_params=None, skip_inversion=False,
inversion_volume_dataset='iceboost',
mb_model_class='MonthlyTIModel',
mb_calibration_strategy='informed_threestep',
geodetic_mb_file_path=None,
temp_bias_file_path=None,
select_source_from_dir=None, keep_dem_folders=False,
add_consensus_thickness=False, add_itslive_velocity=False,
add_millan_thickness=False, add_millan_velocity=False,
add_hugonnet_dhdt=False, add_bedmachine=False,
add_glathida=False, add_distributed_thickness=False,
add_export_thickness_geotiff=False, compute_hypsometry=False,
custom_climate_task=None,
custom_climate_task_kwargs=None,
start_level=None, start_base_url=None,
start_from_dir=None, max_level=5,
chunk_idx=None, chunk_size=1000,
glen_a_factor=None, inversion_fs=0,
logging_level='WORKFLOW',
dynamic_spinup=False, ref_mb_err_scaling_factor=0.2,
dynamic_spinup_start_year=1979,
dynamic_spinup_extra_years_to_try=None,
dynamic_spinup_allow_shorter=True,
continue_on_error=True, store_fl_diagnostics=False,
store_hydro_output=False, store_monthly_hydro=False,
ref_area_yr=None, temp_bias_run=False):
"""Generate the preprocessed OGGM glacier directories for this OGGM version
Parameters
----------
rgi_version : str
the RGI version to use (defaults to cfg.PARAMS)
rgi_reg : str
the RGI region to process
border : int
the number of pixels at the maps border
output_folder : str
path to the output folder (where to put the preprocessed tar files)
dem_source : str
which DEM source to use: default, SOURCE_NAME, STANDARD or ALL
ALL is to generate RGITOPO
"STANDARD" is doina small RGITOPO using COPDEM + NASADEM
default is the current default lookup tables found at
https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/rgitopo/2025.4/
working_dir : str
path to the OGGM working directory
params_file : str
path to the OGGM parameter file (to override defaults)
is_test : bool
to test on a couple of glaciers only! Picks 4 glaciers, always the
same ones (see `test_ids` to choose them): a chunked run needs every
job to select the same glaciers.
test_ids : list
if is_test: list of ids to process
rgi_file : str or geopandas.GeoDataFrame, optional
path to an RGI shapefile or a GeoDataFrame to use instead of
the default RGI region file. Useful to override the default RGI
files for custom runs as well as for testing.
intersects_file : str or geopandas.GeoDataFrame, optional
path to an intersects shapefile or a GeoDataFrame to use instead of
the default RGI intersects file. Can also be None to skip setting
the intersects database.
test_topofile : str
for testing purposes only
test_crudir : str
for testing purposes only
disable_mp : bool
disable multiprocessing
elev_bands : bool
compute all flowlines based on the Huss & Farinotti 2012 method.
centerlines : bool
compute all flowlines based on the OGGM centerline(s) method.
mb_model_class : str
The mb_model_class to use. Options are 'MonthlyTIModel' (default) and
'SfcTypeTIModel'.
mb_calibration_strategy : str
how to calibrate the massbalance. Currently one of:
- 'informed_threestep' (default)
- 'melt_temp'
- 'temp_melt'
geodetic_mb_file_path : str
optional path or URL to a custom geodetic MB file, passed to
utils.get_geodetic_mb_dataframe and
tasks.mb_calibration_from_geodetic_mb.
temp_bias_file_path : str
path or URL to the temperature-bias prior file, passed to
tasks.mb_calibration_from_geodetic_mb. Required by the
'informed_threestep' calibration strategy (and unused otherwise):
there is no default, the file has to match the setup it is used with
(climate dataset, RGI version, ...). It is created with a
`temp_bias_run` and the `oggm_temp_bias` command (see
utils.get_temp_bias_dataframe).
select_source_from_dir : str
if starting from a level 1 "ALL" or "STANDARD" DEM sources directory,
select the chosen DEM source here. If you set it to "BY_RES" here,
COPDEM will be used and its resolution chosen based on the gdir's
map resolution (COPDEM30 for dx < 60 m, COPDEM90 elsewhere).
keep_dem_folders : bool
if `select_source_from_dir` is used, wether to keep the original
DEM folders in or not.
add_consensus_thickness : bool
adds (reprojects) the consensus estimates thickness to the glacier
directories. With elev_bands=True, the data will also be binned.
add_itslive_velocity : bool
adds (reprojects) the ITS_LIVE velocity to the glacier
directories. With elev_bands=True, the data will also be binned.
add_millan_thickness : bool
adds (reprojects) the millan thickness to the glacier
directories. With elev_bands=True, the data will also be binned.
add_millan_velocity : bool
adds (reprojects) the millan velocity to the glacier
directories. With elev_bands=True, the data will also be binned.
add_hugonnet_dhdt : bool
adds (reprojects) the hugonnet dhdt maps to the glacier
directories. With elev_bands=True, the data will also be binned.
add_bedmachine : bool
adds (reprojects) the bedmachine ice thickness maps to the glacier
directories. With elev_bands=True, the data will also be binned.
add_glathida : bool
adds (reprojects) the glathida thickness data to the glacier
directories. Data points are stored as csv files.
add_distributed_thickness : bool
adds a thickness field to gridded_data using
distribute_thickness_per_altitude.
add_export_thickness_geotiff : bool
exports the distributed thickness field to GeoTIFF files in a
subfolder of the L3 summary directory.
compute_hypsometry : bool
Compute the hypsometry tables for all glaciers,
added to the glacier directory and compiled in
the summary folder.
custom_climate_task : str
optional import path to a custom climate task in the form
"module_path:function_name". If provided, it will be called instead of
the default process_climate_data.
custom_climate_task_kwargs : dict
optional kwargs passed to the custom climate task when it is executed.
start_level : str or int
the pre-processed level to start from (default is to start from
scratch). If set, you'll need to indicate start_base_url or
start_from_dir as well. One of 0, 1, 2, '3a', 3, '4a', 4 - see
`max_level` for what the half levels are.
start_base_url : str
the pre-processed base-url to fetch the data from.
start_from_dir : str
like `start_base_url`, but for glacier directory tar files which are
already on disk (a url can only be fetched over http). This is what
chains the stages of a chunked run together: it points at the folder
which contains the `RGI{version}/b_{border}/L{level}/` tree, and the
directories are read from there instead of being downloaded.
It can be combined with `start_base_url`, and often has to be: the
directories then come from disk, while the summary files which are
only carried forward are still fetched from the url. That is the
normal case when the previous stage made the directories without
writing any summary file (`max_level` '3a' or '4a').
max_level : str or int
the maximum pre-processing level before stopping. Besides 1 to 5,
two half levels split L3 and L4 where the work stops being
per-glacier and starts needing the whole RGI region at once:
- '3a': L3 up to and including the apparent mass balance, i.e. no
inversion and no summary files. This is the chunkable part of L3.
- '4a': the L4 runs, without the summary files. This is the chunkable
part of L4.
A chunked cluster run is then 2 -> '3a' (chunks), '3a' -> 3 (whole
region), 3 -> '4a' (chunks), '4a' -> 5 (whole region).
chunk_idx : int
process only the glaciers of this chunk (see `chunk_size`). Chunks are
blocks of the RGI id space, so that several chunk jobs writing into
the same output folder produce disjoint, complete tar files. Use
:py:func:`oggm.workflow.count_rgi_chunks` to know how many chunks a
region has. Default is to process all glaciers.
chunk_size : int
the number of glaciers per chunk: 100 or 1000 (default). These are the
only two allowed, because they are the bundle sizes the glacier
directory tars are written and read with.
glen_a_factor : float
skip the Glen A calibration and invert with this factor instead (and
with `inversion_fs`). The values of a previous calibration are written
to `L3/summary/inversion_glen_a_{rgi_reg}.json` by the run which did
it, so they can be given back here.
inversion_fs : float
the sliding parameter to use together with `glen_a_factor`. Ignored if
`glen_a_factor` is not set.
skip_inversion : bool
do not run the inversion (level 3 files). This is a temporary
workaround for workflows that wont run that far into level 3.
inversion_volume_dataset : str
which reference volume dataset to calibrate the ice thickness
inversion (Glen A) against. One of:
- 'iceboost' (default): the IceBoost v2 product, auto-selected by RGI
version. Supported for RGI62, RGI70G and RGI70C.
- 'consensus': the Farinotti et al. (2019) consensus (ITMIX) estimate.
Only supported for RGI62.
logging_level : str
the logging level to use (DEBUG, INFO, WARNING, WORKFLOW)
override_params : dict
a dict of parameters to override.
dynamic_spinup : str
include a dynamic spinup matching 'area/dmdtda' OR 'volume/dmdtda' at
the RGI-date
ref_mb_err_scaling_factor : float
scaling factor to reduce individual geodetic mass balance uncertainty
dynamic_spinup_start_year : int
if dynamic_spinup is set, define the starting year for the simulation.
The default is 1979, unless the climate data starts later.
dynamic_spinup_extra_years_to_try : list or None
As a last resort, if all other spinup periods failed, you can provide
here a list of years to try to start the spinup *before*
dynamic_spinup_start_year (e.g. [10, 20] means the start years
'dynamic_spinup_start_year - 10' and 'dynamic_spinup_start_year - 20'
are tried, in this order, so the longest spinup is tried last). Start
years before the start of the climate data are clipped to it.
Default is None
dynamic_spinup_allow_shorter : bool
If True, and the spinup starting at dynamic_spinup_start_year was not
successful, shorter spinup periods are tried first (down to the start
year of the geodetic mass balance period), before the
dynamic_spinup_extra_years_to_try. If False, the dynamic spinup never
starts after dynamic_spinup_start_year.
Default is True
continue_on_error : bool
if True the workflow continues if a task raises an error. For operational
runs it should be set to True (the default).
store_fl_diagnostics : bool
if True, also compute and store flowline diagnostics during preprocessing.
This can increase data usage quite a bit.
store_hydro_output : bool
if True, also store the hydrological model output.
store_monthly_hydro : bool
if True and store_hydro_output is True, the hydrological model output
is also stored at monthly resolution (see flowline.run_with_hydro).
This increases data usage quite a bit, hence the False default.
ref_area_yr : int
the hydrological output is computed over a reference area, which
per default is the largest area covered by the glacier in the simulation
period. Use this kwarg to force a specific area to the state of the
glacier at the provided simulation year.
temp_bias_run : bool
set to True to run the preprocessing needed to create the temperature
bias prior file used by the `informed_threestep` calibration. This is
a preset which forces `max_level=3` and `skip_inversion=True`, and
skips everything which is of no use for this purpose: the glacier
directory tar files, the climate statistics and the fixed geometry
mass balance. `mb_calibration_strategy` has to be set explicitly to
`temp_melt`, an error is raised otherwise.
The only output is the L3 `glacier_statistics` file, which is then
turned into the temperature bias file with the `oggm_temp_bias`
command (the grouping of climate grid points crosses RGI region
borders, so this has to be done over all the regions at once).
"""
# The temp bias preset overrides a couple of options. We log about it
# further down, once cfg.initialize() has set the logging up.
if temp_bias_run:
if mb_calibration_strategy != 'temp_melt':
raise InvalidParamsError(
'With `temp_bias_run`, the mass balance calibration strategy '
'has to be set explicitly to `temp_melt`, not '
f'`{mb_calibration_strategy}`.')
max_level = 3
skip_inversion = True
# Input check. The levels are strings so that they can carry the two half
# levels '3a' and '4a', but everything below works on the integer part
# plus a flag - see `_parse_max_level` / `_parse_start_level`.
max_level_name = str(max_level)
max_level, stop_before_inversion, skip_summary = _parse_max_level(max_level)
start_level_name = '0' if start_level is None else str(start_level)
start_level, resume_at_inversion, summary_only = \
_parse_start_level(start_level)
if mb_calibration_strategy not in ['informed_threestep', 'melt_temp',
'temp_melt']:
raise InvalidParamsError('mb_calibration_strategy not understood: '
f'{mb_calibration_strategy}')
if start_level_name != '0':
if start_base_url is None and start_from_dir is None:
raise InvalidParamsError('With start_level, please also indicate '
'start_base_url or start_from_dir')
# We log about this further down, once cfg.initialize() has set the
# logging up - `log.workflow` does not exist before that
ignoring_intersects = start_level_name != '0' and intersects_file is not None
if start_level_name == '0' and start_from_dir is not None:
raise InvalidParamsError('start_from_dir needs a start_level: with '
'level 0 the glacier directories are built '
'from the RGI file, not read from tars.')
if stop_before_inversion and skip_inversion:
# `3a` exists to hand the directories over to the whole-region job
# which does the inversion, and that job needs the apparent mass
# balance - which skip_inversion is what skips
raise InvalidParamsError('max_level `3a` and skip_inversion cannot be '
'combined: `3a` stops right before the '
'inversion so that another run can do it, '
'while skip_inversion drops it altogether.')
if PREPRO_LEVELS.index(max_level_name) <= \
PREPRO_LEVELS.index(start_level_name):
raise InvalidParamsError(
f'max_level ({max_level_name}) should be above start_level '
f'({start_level_name}).')
if chunk_idx is not None:
# Fail early on a bad chunk_size, rather than after the RGI file has
# been read (this validates it and returns 0 on the empty list)
workflow.count_rgi_chunks([], chunk_size=chunk_size)
if chunk_idx < 0:
raise InvalidParamsError('chunk_idx should be positive, got '
f'{chunk_idx}')
# The mass balance is calibrated in L3 only - and not by a run which
# resumes at the inversion, since that part is already done by then
if (start_level <= 2 and max_level >= 3 and not resume_at_inversion and
mb_calibration_strategy == 'informed_threestep' and
temp_bias_file_path is None):
raise InvalidParamsError(
'The `informed_threestep` calibration strategy needs a temperature '
'bias prior file: set `temp_bias_file_path` to the file matching '
'your setup. Such a file is created with a `temp_bias_run` and '
'the `oggm_temp_bias` command.')
if dynamic_spinup:
if dynamic_spinup not in ['area/dmdtda', 'volume/dmdtda']:
raise InvalidParamsError(f"Dynamic spinup option '{dynamic_spinup}' "
"not supported")
# Time
start = time.time()
def _time_log():
# Log util
m, s = divmod(time.time() - start, 60)
h, m = divmod(m, 60)
log.workflow('OGGM prepro_levels is done! Time needed: '
'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s)))
# Local paths
if override_params is None:
override_params = {}
# Use multiprocessing?
override_params['use_multiprocessing'] = not disable_mp
# How many grid points around the glacier?
# Make it large if you expect your glaciers to grow large
override_params['border'] = border
# Some arbitrary heuristics on the length of tidewater extension
extension = int(utils.clip_min(border / 2, 30))
override_params['calving_line_extension'] = extension
# Set to True for operational runs
override_params['continue_on_error'] = continue_on_error
# For centerlines we have to change the default evolution model and bed
if centerlines:
override_params['downstream_line_shape'] = 'parabola'
override_params['evolution_model'] = 'FluxBased'
# define the default melt_f depending on the the used mb_model_class
if mb_model_class == 'MonthlyTIModel':
mb_model_class = MonthlyTIModel
store_mb_diagnostics = False
elif mb_model_class == 'SfcTypeTIModel':
mb_model_class = SfcTypeTIModel
store_mb_diagnostics = True
else:
raise NotImplementedError(f"Unknown mb_model: {mb_model_class}")
# Other things that make sense
override_params['store_model_geometry'] = True
override_params['store_fl_diagnostics'] = store_fl_diagnostics
utils.mkdir(working_dir)
override_params['working_dir'] = working_dir
# Initialize OGGM and set up the run parameters
cfg.initialize(file=params_file, params=override_params,
logging_level=logging_level)
# Prepare the download of climate file to be shared across processes
# TODO
if ignoring_intersects:
log.workflow('`intersects_file` is ignored with start_level > 0: '
'the intersects are written to the glacier '
'directories at L0 and are already in the prepro '
'files we start from.')
if temp_bias_run:
log.workflow('`temp_bias_run` is set: forcing max_level=3 and '
'skip_inversion=True. The only output will be the L3 '
'glacier statistics file: no glacier directory tar '
'files, no climate statistics, no fixed geometry mass '
'balance.')
# Log the parameters
msg = '# OGGM Run parameters:'
for k, v in cfg.PARAMS.items():
if type(v) in [pd.DataFrame, dict]:
continue
msg += '\n {}: {}'.format(k, v)
log.workflow(msg)
if rgi_version is None:
rgi_version = cfg.PARAMS['rgi_version']
output_base_dir = Path(output_folder) / f'RGI{rgi_version}' / f'b_{border:03d}'
# Add a package version file
utils.mkdir(output_base_dir)
opath = output_base_dir / 'package_versions.txt'
with open(opath, 'w') as vfile:
vfile.write(utils.show_versions(logger=log))
if rgi_file is None:
# Get the RGI file
rgidf = gpd.read_file(utils.get_rgi_region_file(rgi_reg,
version=rgi_version))
# We use intersects. They are only needed to build the glacier
# directories from the RGI (L0): from L1 on, each directory has its
# own intersects.shp and the region wide file is never read again
if rgi_version != '70C' and start_level == 0:
if intersects_file is None:
rgif = utils.get_rgi_intersects_region_file(rgi_reg,
version=rgi_version)
else:
rgif = intersects_file
cfg.set_intersects_db(rgif)
rgidf = apply_rgi_fixes(rgidf, rgi_version, rgi_reg)
else:
if isinstance(rgi_file, str):
rgidf = gpd.read_file(rgi_file)
else:
rgidf = rgi_file
if start_level == 0:
cfg.set_intersects_db(intersects_file)
if is_test:
if test_ids is not None:
try:
rgidf = rgidf.loc[rgidf.RGIId.isin(test_ids)]
except AttributeError:
# RGI7
rgidf = rgidf.loc[rgidf.rgi_id.isin(test_ids)]
else:
# Seeded for chucked runs
rgidf = rgidf.sample(4, random_state=0)
if len(rgidf) == 0:
raise InvalidParamsError('Zero glaciers selected!')
# Select our chunk of glaciers, if any. This has to happen after all the
# filtering above (region 05 connectivity, the RGI62 fixes, ...) so that
# a given chunk index always means the same glaciers. What comes after
# (the 70C dates, the DEM source lookup) are per-glacier maps and works
# just as well on a subset.
if chunk_idx is not None:
n_chunks = workflow.count_rgi_chunks(rgidf, chunk_size=chunk_size)
rgidf = workflow.get_rgi_chunk(rgidf, chunk_idx,
chunk_size=chunk_size)
log.workflow('Selected chunk {} of {} (chunk size {}): {} glaciers.'
''.format(chunk_idx, n_chunks, chunk_size, len(rgidf)))
if len(rgidf) == 0:
# Chunks are blocks of the RGI id space, and the ids have gaps,
# so an empty chunk is a normal thing. Stop here, but without an
# error: on a cluster this is one task of an array job, and a
# failure would take the dependent jobs down with it.
log.workflow('This chunk is empty, nothing to do.')
_time_log()
return
log.workflow('Starting prepro run for RGI reg: {} '
'and border: {}'.format(rgi_reg, border))
log.workflow('Number of glaciers: {}'.format(len(rgidf)))
# Try to avoid concurrency
if rgi_version == '70C':
fp = file_downloader('https://cluster.klima.uni-bremen.de/~oggm/'
'ref_mb_params/oggm_v1.6/inv_rgi7/'
'rgi7c_rgi_year_2025.1.csv')
rgi_year_by_id = pd.read_csv(fp, index_col=0)['rgi_year'].astype(int).astype(str)
rgidf['src_date'] = rgidf['rgi_id'].map(rgi_year_by_id) + '-01-01 00:00:00'
# Add a new default source
if not dem_source:
fs_url = 'https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/rgitopo/2025.4/'
if rgi_version == '62':
fs = utils.file_downloader(fs_url + 'chosen_dem_RGI62_20251029.csv')
dfs = pd.read_csv(fs, index_col=0)
rgidf['dem_source'] = dfs.loc[rgidf['RGIId'], 'dem_source'].values
if rgi_version == '70G':
fs = utils.file_downloader(fs_url + 'chosen_dem_RGI70G_20251029.csv')
dfs = pd.read_csv(fs, index_col=0)
rgidf['dem_source'] = dfs.loc[rgidf['rgi_id'], 'dem_source'].values
if rgi_version == '70C':
fs = utils.file_downloader(fs_url + 'chosen_dem_RGI70C_20251029.csv')
dfs = pd.read_csv(fs, index_col=0)
rgidf['dem_source'] = dfs.loc[rgidf['rgi_id'], 'dem_source'].values
# L0 - go
if start_level == 0:
gdirs = workflow.init_glacier_directories(rgidf, reset=True, force=True)
# Glacier stats
sum_dir = Path(output_base_dir) / 'L0' / 'summary'
utils.mkdir(sum_dir)
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
# L0 OK - compress all in output directory
if not temp_bias_run:
log.workflow('L0 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L0'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
if max_level == 0:
_time_log()
return
elif start_from_dir is not None:
# The tar files are already on disk (this is how the stages of a
# chunked run are chained together)
from_tar = _level_dir(start_from_dir, rgi_version, border,
start_level_name)
if not from_tar.is_dir():
raise InvalidParamsError('Could not find the glacier directories '
f'to start from in {from_tar}')
log.workflow(f'Reading the L{start_level_name} glacier directories '
f'from {from_tar}')
gdirs = workflow.init_glacier_directories(rgidf, reset=True,
force=True,
from_tar=str(from_tar))
else:
# The level to fetch is the one the directories are *stored* under,
# which is not the integer we use for the level logic: resuming at
# '4a' means reading the L4 directories, and start_level is 3 there.
prepro_level = _level_dir_name(start_level_name)[1:]
gdirs = workflow.init_glacier_directories(rgidf, reset=True, force=True,
from_prepro_level=prepro_level,
prepro_border=border,
prepro_rgi_version=rgi_version,
prepro_base_url=start_base_url
)
# L1 - Add dem files
if start_level == 0:
if test_topofile:
cfg.PATHS['dem_file'] = test_topofile
# Which DEM source?
if dem_source.upper() in ['ALL', 'STANDARD']:
# This is the complex one, just do the job and leave
if dem_source.upper() == 'ALL':
sources = utils.DEM_SOURCES
if dem_source.upper() == 'STANDARD':
sources = ['COPDEM30', 'COPDEM90', 'NASADEM']
log.workflow('Running prepro on several sources')
for i, s in enumerate(sources):
rs = i == 0
log.workflow('Running prepro on sources: {}'.format(s))
gdirs = workflow.init_glacier_directories(rgidf, reset=rs,
force=rs)
workflow.execute_entity_task(tasks.define_glacier_region, gdirs,
source=s)
workflow.execute_entity_task(_rename_dem_folder, gdirs, source=s)
# make a GeoTiff mask of the glacier, choose any source
workflow.execute_entity_task(gis.rasterio_glacier_mask,
gdirs, source='ALL')
# Glacier stats
sum_dir = Path(output_base_dir) / 'L1' / 'summary'
utils.mkdir(sum_dir)
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
# Add hypsometry files
if compute_hypsometry:
for dem_source in utils.DEM_SOURCES:
from oggm.shop.rgitopo import select_dem_from_dir
workflow.execute_entity_task(select_dem_from_dir, gdirs,
dem_source=dem_source,
keep_dem_folders=True)
workflow.execute_entity_task(tasks.rasterio_glacier_mask, gdirs,
no_nunataks=True,
overwrite=False)
workflow.execute_entity_task(tasks.rasterio_glacier_exterior_mask,
gdirs,
overwrite=False)
workflow.execute_entity_task(tasks.compute_hypsometry_attributes, gdirs)
opath = sum_dir / f'hypsometry_{rgi_reg}_{dem_source}.csv'
utils.compile_glacier_hypsometry(gdirs, path=opath,
add_column=('dem_source', dem_source))
workflow.execute_entity_task(_move_hypsometry_to_dem_folder,
gdirs, source=dem_source)
# L1 OK - compress all in output directory
if not temp_bias_run:
log.workflow('L1 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L1'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs,
delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
_time_log()
return
# Force a given source
source = dem_source.upper() if dem_source else None
# L1 - go
workflow.execute_entity_task(tasks.define_glacier_region, gdirs,
source=source)
# Summaries
sum_dir = Path(output_base_dir) / 'L1' / 'summary'
utils.mkdir(sum_dir)
# Add hypsometry files
if compute_hypsometry:
workflow.execute_entity_task(tasks.rasterio_glacier_mask, gdirs)
workflow.execute_entity_task(tasks.rasterio_glacier_mask, gdirs,
no_nunataks=True)
workflow.execute_entity_task(tasks.rasterio_glacier_exterior_mask, gdirs)
workflow.execute_entity_task(tasks.compute_hypsometry_attributes, gdirs)
opath = sum_dir / f'hypsometry_{rgi_reg}.csv'
utils.compile_glacier_hypsometry(gdirs, path=opath)
# Glacier stats
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
# L1 OK - compress all in output directory
if not temp_bias_run:
log.workflow('L1 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L1'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
if max_level == 1:
_time_log()
return
# L2 - Tasks
if start_level <= 1:
# Check which glaciers will be processed as what
if elev_bands:
gdirs_band = gdirs
gdirs_cent = []
elif centerlines:
gdirs_band = []
gdirs_cent = gdirs
else:
raise InvalidParamsError('Need to specify if `elev_bands` or '
'`centerlines` type.')
log.workflow('Start flowline processing with: '
'N centerline type: {}, '
'N elev bands type: {}.'
''.format(len(gdirs_cent), len(gdirs_band)))
# If we are coming from a multi-dem setup, let's select it from there
if select_source_from_dir is not None:
from oggm.shop.rgitopo import select_dem_from_dir
workflow.execute_entity_task(select_dem_from_dir, gdirs_band,
dem_source=select_source_from_dir,
keep_dem_folders=keep_dem_folders)
workflow.execute_entity_task(select_dem_from_dir, gdirs_cent,
dem_source=select_source_from_dir,
keep_dem_folders=keep_dem_folders)
# HH2015 method
workflow.execute_entity_task(tasks.simple_glacier_masks, gdirs_band)
# Centerlines OGGM
workflow.execute_entity_task(tasks.glacier_masks, gdirs_cent)
bin_variables = []
if add_consensus_thickness:
from oggm.shop.bedtopo import add_consensus_thickness
workflow.execute_entity_task(add_consensus_thickness, gdirs)
bin_variables.append('consensus_ice_thickness')
if add_itslive_velocity:
from oggm.shop.its_live import itslive_velocity_to_gdir
workflow.execute_entity_task(itslive_velocity_to_gdir, gdirs)
bin_variables.append('itslive_v')
if add_millan_thickness:
from oggm.shop.millan22 import millan_thickness_to_gdir
workflow.execute_entity_task(millan_thickness_to_gdir, gdirs)
bin_variables.append('millan_ice_thickness')
if add_millan_velocity:
from oggm.shop.millan22 import millan_velocity_to_gdir
workflow.execute_entity_task(millan_velocity_to_gdir, gdirs)
bin_variables.append('millan_v')
if add_hugonnet_dhdt:
from oggm.shop.hugonnet_maps import hugonnet_to_gdir
workflow.execute_entity_task(hugonnet_to_gdir, gdirs)
bin_variables.append('hugonnet_dhdt')
if add_bedmachine:
from oggm.shop.bedmachine import bedmachine_to_gdir
workflow.execute_entity_task(bedmachine_to_gdir, gdirs)
bin_variables.append('bedmachine_ice_thickness')
if add_glathida:
from oggm.shop.glathida import glathida_to_gdir
workflow.execute_entity_task(glathida_to_gdir, gdirs)
if rgi_version == '70C':
# Some additional data for the 70C glaciers
workflow.execute_entity_task(tasks.rgi7g_to_complex, gdirs)
if bin_variables and gdirs_band:
workflow.execute_entity_task(tasks.elevation_band_flowline,
gdirs_band,
bin_variables=bin_variables)
workflow.execute_entity_task(tasks.fixed_dx_elevation_band_flowline,
gdirs_band,
bin_variables=bin_variables)
else:
# HH2015 method without it
task_list = [
tasks.elevation_band_flowline,
tasks.fixed_dx_elevation_band_flowline,
]
for task in task_list:
workflow.execute_entity_task(task, gdirs_band)
# Centerlines OGGM
task_list = [
tasks.compute_centerlines,
tasks.initialize_flowlines,
tasks.catchment_area,
tasks.catchment_intersections,
tasks.catchment_width_geom,
tasks.catchment_width_correction,
]
for task in task_list:
workflow.execute_entity_task(task, gdirs_cent)
# Same for all glaciers
if border >= 20:
task_list = [
tasks.compute_downstream_line,
tasks.compute_downstream_bedshape,
]
for task in task_list:
workflow.execute_entity_task(task, gdirs)
else:
log.workflow('L2: for map border values < 20, wont compute '
'downstream lines.')
# Glacier stats
sum_dir = Path(output_base_dir) / 'L2' / 'summary'
utils.mkdir(sum_dir)
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
if add_itslive_velocity:
from oggm.shop.its_live import compile_itslive_statistics
opath = sum_dir / f'itslive_statistics_{rgi_reg}.csv'
compile_itslive_statistics(gdirs, path=opath)
if add_millan_thickness or add_millan_velocity:
from oggm.shop.millan22 import compile_millan_statistics
opath = sum_dir / f'millan_statistics_{rgi_reg}.csv'
compile_millan_statistics(gdirs, path=opath)
if add_consensus_thickness:
from oggm.shop.bedtopo import compile_consensus_statistics
opath = sum_dir / f'consensus_statistics_{rgi_reg}.csv'
compile_consensus_statistics(gdirs, path=opath)
if add_hugonnet_dhdt:
from oggm.shop.hugonnet_maps import compile_hugonnet_statistics
opath = sum_dir / f'hugonnet_statistics_{rgi_reg}.csv'
compile_hugonnet_statistics(gdirs, path=opath)
if add_bedmachine:
from oggm.shop.bedmachine import compile_bedmachine_statistics
opath = sum_dir / f'bedmachine_statistics_{rgi_reg}.csv'
compile_bedmachine_statistics(gdirs, path=opath)
if add_glathida:
from oggm.shop.glathida import compile_glathida_statistics
opath = sum_dir / f'glathida_statistics_{rgi_reg}.csv'
compile_glathida_statistics(gdirs, path=opath)
# And for level 2: shapes
if len(gdirs_cent) > 0:
opath = sum_dir / f'centerlines_{rgi_reg}.shp'
utils.write_centerlines_to_shape(gdirs_cent, to_tar=True,
path=opath)
opath = sum_dir / f'centerlines_smoothed_{rgi_reg}.shp'
utils.write_centerlines_to_shape(gdirs_cent, to_tar=True,
ensure_exterior_match=True,
simplify_line_before=0.75,
corner_cutting=3,
path=opath)
opath = sum_dir / f'flowlines_{rgi_reg}.shp'
utils.write_centerlines_to_shape(gdirs_cent, to_tar=True,
flowlines_output=True,
path=opath)
opath = sum_dir / f'geom_widths_{rgi_reg}.shp'
utils.write_centerlines_to_shape(gdirs_cent, to_tar=True,
geometrical_widths_output=True,
path=opath)
opath = sum_dir / f'widths_{rgi_reg}.shp'
utils.write_centerlines_to_shape(gdirs_cent, to_tar=True,
corrected_widths_output=True,
path=opath)
# L2 OK - compress all in output directory
if not temp_bias_run:
log.workflow('L2 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L2'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
if max_level == 2:
_time_log()
return
# L3 - Tasks
if start_level <= 2:
sum_dir = Path(output_base_dir) / 'L3' / 'summary'
# Everything down to the apparent mass balance is per-glacier, so it
# is the part of L3 which can be run in chunks. A run resuming at
# `3a` has it done already and goes straight to the inversion.
if resume_at_inversion:
log.workflow('Resuming L3 at the inversion: the climate and the '
'mass balance calibration are read from the glacier '
'directories we started from.')
else:
# Climate
climate_kwargs = custom_climate_task_kwargs or {}
if custom_climate_task:
try:
mod_path, func_name = custom_climate_task.rsplit(':', 1)
except ValueError:
raise InvalidParamsError('custom_climate_task must be of the form "module:function"')
try:
mod = importlib.import_module(mod_path)
except ModuleNotFoundError as err:
raise InvalidParamsError(f'Cannot import module {mod_path}') from err
try:
custom_task_func = getattr(mod, func_name)
except AttributeError as err:
raise InvalidParamsError(f'Module {mod_path} has no attribute {func_name}') from err
workflow.execute_entity_task(custom_task_func, gdirs, **climate_kwargs)
else:
workflow.execute_entity_task(tasks.process_climate_data, gdirs)
if mb_calibration_strategy == 'informed_threestep':
workflow.execute_entity_task(tasks.mb_calibration_from_geodetic_mb,
gdirs,
informed_threestep=True,
mb_model_class=mb_model_class,
file_path=geodetic_mb_file_path,
temp_bias_file_path=temp_bias_file_path)
elif mb_calibration_strategy == 'melt_temp':
workflow.execute_entity_task(tasks.mb_calibration_from_geodetic_mb,
gdirs,
calibrate_param1='melt_f',
calibrate_param2='temp_bias',
mb_model_class=mb_model_class,
file_path=geodetic_mb_file_path)
elif mb_calibration_strategy == 'temp_melt':
workflow.execute_entity_task(tasks.mb_calibration_from_geodetic_mb,
gdirs,
calibrate_param1='temp_bias',
calibrate_param2='melt_f',
mb_model_class=mb_model_class,
file_path=geodetic_mb_file_path)
else:
raise InvalidParamsError('mb_calibration_strategy not understood: '
f'{mb_calibration_strategy}')
if not skip_inversion:
workflow.execute_entity_task(tasks.apparent_mb_from_any_mb,
gdirs,
mb_model_class=mb_model_class,)
if stop_before_inversion:
# End of the chunkable part of L3. Write the directories out so
# that a whole-region job can calibrate Glen A and finish the
# level: that calibration needs all the glaciers at once.
log.workflow('L3a done (no inversion, no summary). '
'Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L3a'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
_time_log()
return
utils.mkdir(sum_dir)
if not skip_inversion:
filter = border >= 20
# Inversion: calibrate Glen A to a reference volume dataset.
# Be explicit about which dataset is used for which RGI version.
if inversion_volume_dataset not in ('iceboost', 'consensus'):
raise InvalidParamsError(
"inversion_volume_dataset must be 'iceboost' or "
f"'consensus', not '{inversion_volume_dataset}'.")
if rgi_version in ('70G', '70C') and \
inversion_volume_dataset != 'iceboost':
raise InvalidParamsError(
f"For {rgi_version} only inversion_volume_dataset='iceboost' "
f"is supported, not '{inversion_volume_dataset}' (the "
"consensus estimate is only available for RGI62).")
# 'iceboost'/'consensus' map directly to ref_table presets
inv_df = workflow.calibrate_inversion_from_ref_table(
gdirs,
ref_table=inversion_volume_dataset,
glen_a_factor=glen_a_factor,
fs=inversion_fs,
apply_fs_on_mismatch=True,
error_on_mismatch=False,
filter_inversion_output=filter)
# Write down which Glen A this region ended up with, so that a
# later run can reproduce it with `glen_a_factor` instead of
# calibrating again (the calibration needs the whole region)
opath = sum_dir / f'inversion_glen_a_{rgi_reg}.json'
with open(opath, 'w') as f:
json.dump({'rgi_version': rgi_version,
'rgi_reg': rgi_reg,
'border': border,
'ref_table': inversion_volume_dataset,
'n_glaciers': len(gdirs),
'glen_a_factor': float(inv_df.attrs['glen_a_factor']),
'glen_a': float(inv_df.attrs['glen_a']),
'fs': float(inv_df.attrs['fs']),
}, f, indent=2)
# Distribute thickness per altitude for gridded data
if add_distributed_thickness:
workflow.execute_entity_task(tasks.distribute_thickness_per_altitude, gdirs)
# We get ready for modelling
if border >= 20:
workflow.execute_entity_task(tasks.init_present_time_glacier, gdirs)
else:
log.workflow('L3: for map border values < 20, wont initialize glaciers '
'for the run.')
# Glacier stats
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
if temp_bias_run:
# The glacier statistics is all we need: the temperature bias file
# itself is made by the `oggm_temp_bias` command, out of the
# statistics of all the RGI regions at once.
log.workflow('`temp_bias_run` is done. Now run the '
'`oggm_temp_bias` command on the L3 summary folder '
'of all the regions to create the temperature bias '
'file.')
_time_log()
return
# Export thickness to GeoTIFF if requested
if add_export_thickness_geotiff and add_distributed_thickness:
thickness_dir = sum_dir / 'distributed_thickness'
utils.mkdir(thickness_dir)
workflow.execute_entity_task(tasks.gridded_data_var_to_geotiff, gdirs,
varname='distributed_thickness',
output_folder=thickness_dir)
opath = sum_dir / f'climate_statistics_{rgi_reg}.csv'
utils.compile_climate_statistics(gdirs, path=opath)
opath = sum_dir / f'fixed_geometry_mass_balance_{rgi_reg}.csv'
utils.compile_fixed_geometry_mass_balance(gdirs, path=opath,
mb_model_class=mb_model_class)
# L3 OK - compress all in output directory
log.workflow('L3 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L3'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
if max_level == 3:
_time_log()
return
if border < 20:
log.workflow('L3: for map border values < 20, wont compute L4 and L5.')
_time_log()
return
# is needed to copy some files for L4 and L5
sum_dir_L3 = sum_dir
# L4 - Tasks (add historical runs (old default) and dynamic spinup runs)
if start_level <= 3:
sum_dir = Path(output_base_dir) / 'L4' / 'summary'
# The summary files are written by the whole-region part of L4, so a
# chunked run stopping at `4a` has nothing to do with them
if not skip_summary:
utils.mkdir(sum_dir)
# Copy L3 files for consistency
for bn in ['glacier_statistics', 'climate_statistics',
'fixed_geometry_mass_balance']:
if start_level <= 2:
ipath = sum_dir_L3 / f'{bn}_{rgi_reg}.csv'
else:
ipath = _forward_summary_path(
f'{bn}_{rgi_reg}.csv', start_level,
start_from_dir, start_base_url, rgi_version, border)
opath = sum_dir / f'{bn}_{rgi_reg}.csv'
shutil.copyfile(ipath, opath)
# The runs are per-glacier: this is the part of L4 which can be run
# in chunks. A run resuming at `4a` has them done already and only
# needs to compile the summary files, which needs the whole region.
if summary_only:
log.workflow('Resuming L4 at the summary files: the model runs '
'are read from the glacier directories we started '
'from.')
else:
# Get end date. The first gdir might have blown up, try some others
i = 0
while True:
if i >= len(gdirs):
raise RuntimeError('Found no valid glaciers!')
try:
y0 = gdirs[i].get_climate_info()['baseline_yr_0']
# One adds 1 because the run ends at the end of the year
ye = gdirs[i].get_climate_info()['baseline_yr_1'] + 1
break
except BaseException:
i += 1
# here we define the actual start date of the model outputs
if y0 > dynamic_spinup_start_year:
dynamic_spinup_start_year = y0
# conduct historical run before dynamic melt_f calibration
# (for comparison to old default behavior)
kwargs_run_from_climate_data = {
'min_ys': y0, 'ye': ye, 'mb_model_class': mb_model_class,
'save_mb_diagnostics_filesuffix': '_historical' if store_mb_diagnostics else None,
'output_filesuffix': '_historical',
'fixed_geometry_spinup_yr': dynamic_spinup_start_year,
}
if not store_hydro_output:
workflow.execute_entity_task(
tasks.run_from_climate_data, gdirs,
**kwargs_run_from_climate_data
)
else:
workflow.execute_entity_task(
tasks.run_with_hydro, gdirs,
run_task=tasks.run_from_climate_data,
store_monthly_hydro=store_monthly_hydro,
ref_area_yr=ref_area_yr,
**kwargs_run_from_climate_data
)
if not skip_summary:
# Now compile the output
opath = Path(sum_dir) / f'historical_run_output_{rgi_reg}.nc'
utils.compile_run_output(gdirs, path=opath,
input_filesuffix='_historical')
# conduct dynamic spinup if wanted
if dynamic_spinup:
if not summary_only:
minimise_for = dynamic_spinup.split('/')[0]
melt_f_max = cfg.PARAMS['melt_f_max']
kwargs_run_dynamic_melt_f_calibration = {
'ref_mb_err_scaling_factor': ref_mb_err_scaling_factor,
'ys': dynamic_spinup_start_year, 'ye': ye,
'melt_f_max': melt_f_max,
'mb_model_class': mb_model_class,
'kwargs_run_function': {
'minimise_for': minimise_for,
'spinup_extra_years_to_try':
dynamic_spinup_extra_years_to_try,
'allow_shorter_spinup': dynamic_spinup_allow_shorter,
},
'ignore_errors': True,
'kwargs_fallback_function': {
'minimise_for': minimise_for,
'spinup_extra_years_to_try':
dynamic_spinup_extra_years_to_try,
'allow_shorter_spinup': dynamic_spinup_allow_shorter,
},
'save_mb_diagnostics_filesuffix': ('_spinup_historical'
if store_mb_diagnostics else None),
'output_filesuffix': '_spinup_historical',
}
if not store_hydro_output:
workflow.execute_entity_task(
tasks.run_dynamic_melt_f_calibration, gdirs,
**kwargs_run_dynamic_melt_f_calibration
)
else:
workflow.execute_entity_task(
tasks.run_with_hydro, gdirs,
run_task=tasks.run_dynamic_melt_f_calibration,
store_monthly_hydro=store_monthly_hydro,
ref_area_yr=ref_area_yr,
**kwargs_run_dynamic_melt_f_calibration
)
if not skip_summary:
# Now compile the output
opath = sum_dir / f'spinup_historical_run_output_{rgi_reg}.nc'
utils.compile_run_output(gdirs, path=opath,
input_filesuffix='_spinup_historical')
if not skip_summary:
# Glacier statistics we recompute here for error analysis
opath = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
utils.compile_glacier_statistics(gdirs, path=opath)
# Add the extended files
pf = sum_dir / f'historical_run_output_{rgi_reg}.nc'
# We have copied the files above
mf = sum_dir / f'fixed_geometry_mass_balance_{rgi_reg}.csv'
sf = sum_dir / f'glacier_statistics_{rgi_reg}.csv'
opath = sum_dir / f'historical_run_output_extended_{rgi_reg}.nc'
utils.extend_past_climate_run(past_run_file=pf,
fixed_geometry_mb_file=mf,
glacier_statistics_file=sf,
path=opath)
# L4 OK - compress all in output directory. A run which only added
# the summary files did not touch the directories, and the `4a` stage
# has already written them to the very same folder.
if not summary_only:
log.workflow('L4 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L4'
workflow.execute_entity_task(utils.gdir_to_tar, gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
sum_dir_L4 = sum_dir
if max_level == 4:
_time_log()
return
# L5 - No tasks: make the dirs small
sum_dir = Path(output_base_dir) / 'L5' / 'summary'
utils.mkdir(sum_dir)
# Copy L4 files for consistency
files_to_copy = ['glacier_statistics', 'climate_statistics',
'fixed_geometry_mass_balance', 'historical_run_output',
'historical_run_output_extended']
files_suffixes = ['csv', 'csv', 'csv', 'nc', 'nc']
if dynamic_spinup:
files_to_copy.append('spinup_historical_run_output')
files_suffixes.append('nc')
for bn, suffix in zip(files_to_copy, files_suffixes):
if start_level <= 3:
ipath = sum_dir_L4 / f'{bn}_{rgi_reg}.{suffix}'
else:
ipath = _forward_summary_path(
f'{bn}_{rgi_reg}.{suffix}', start_level,
start_from_dir, start_base_url, rgi_version, border)
opath = sum_dir / f'{bn}_{rgi_reg}.{suffix}'
shutil.copyfile(ipath, opath)
# Copy mini data to new dir
mini_base_dir = (
Path(working_dir)
/ 'mini_perglacier'
/ f'RGI{rgi_version}'
/ f'b_{border:03d}'
)
mini_gdirs = workflow.execute_entity_task(tasks.copy_to_basedir, gdirs,
base_dir=mini_base_dir,
setup='run/spinup')
# L5 OK - compress all in output directory
log.workflow('L5 done. Writing to tar...')
level_base_dir = Path(output_base_dir) / 'L5'
workflow.execute_entity_task(utils.gdir_to_tar, mini_gdirs, delete=False,
base_dir=level_base_dir)
utils.base_dir_to_tar(level_base_dir)
_time_log()
def parse_args(args):
"""Check input arguments and env variables"""
# CLI args
description = ('Generate the preprocessed OGGM glacier directories for '
'this OGGM version.')
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--map-border', type=int,
help='the size of the map border. Is required if '
'$OGGM_MAP_BORDER is not set.')
parser.add_argument('--rgi-reg', type=str,
help='the rgi region to process. Is required if '
'$OGGM_RGI_REG is not set.')
parser.add_argument('--rgi-version', type=str,
help='the RGI version to use. Defaults to the OGGM '
'default.')
parser.add_argument('--start-level', type=str, default='0',
choices=PREPRO_LEVELS[:-1],
help='the pre-processed level to start from (default '
'is to start from 0). If set, you will need to '
'indicate --start-base-url or --start-from-dir '
'as well. See --max-level for the half levels.')
parser.add_argument('--start-base-url', type=str,
help='the pre-processed base-url to fetch the data '
'from when starting from level > 0.')
parser.add_argument('--start-from-dir', type=str,
help='like --start-base-url, but for glacier '
'directory tar files which are already on disk. '
'This is what chains the stages of a chunked '
'run together. Point it at the folder holding '
'the RGI{version}/b_{border}/L{level}/ tree. Can '
'be combined with --start-base-url, which is '
'then used for the summary files which are only '
'carried forward.')
parser.add_argument('--max-level', type=str, default='5',
choices=PREPRO_LEVELS[1:],
help='the maximum level you want to run the '
'pre-processing for. Besides 1 to 5, the two '
'half levels 3a and 4a stop where the work stops '
'being per-glacier and starts needing the whole '
'RGI region: 3a is L3 without the inversion and '
'the summary files, 4a is L4 without the summary '
'files. A chunked cluster run is 2 -> 3a '
'(chunks), 3a -> 3 (region), 3 -> 4a (chunks), '
'4a -> 5 (region).')
parser.add_argument('--chunk-idx', type=int, default=None,
help='process only the glaciers of this chunk. Chunks '
'are blocks of the RGI id space, so that several '
'chunk jobs writing into the same output folder '
'produce disjoint, complete tar files. Meant to '
'be set to $SLURM_ARRAY_TASK_ID. Use the '
'oggm_prepro_chunks command to know how many '
'chunks a region has.')
parser.add_argument('--chunk-size', type=int, default=1000,
choices=[100, 1000],
help='the number of glaciers per chunk (default '
'1000). Only 100 and 1000 are allowed: they are '
'the bundle sizes the glacier directory tars are '
'written and read with.')
parser.add_argument('--inversion-glen-a-factor', type=float, default=None,
help='skip the Glen A calibration and invert with '
'this factor instead. The value of a previous '
'calibration is written to the L3 summary folder '
'as inversion_glen_a_{rgi_reg}.json.')
parser.add_argument('--inversion-fs', type=float, default=0,
help='the sliding parameter for the inversion. Mostly '
'useful together with --inversion-glen-a-factor, '
'to reproduce a calibration which needed it.')
parser.add_argument('--working-dir', type=str,
help='path to the directory where to write the '
'output. Defaults to current directory or '
'$OGGM_WORKDIR.')
parser.add_argument('--params-file', type=str,
help='path to the OGGM parameter file to use in place '
'of the default one.')
parser.add_argument('--output', type=str,
help='path to the directory where to write the '
'output. Defaults to current directory or '
'$OGGM_OUTDIR.')
parser.add_argument('--logging-level', type=str, default='WORKFLOW',
help='the logging level to use (DEBUG, INFO, WARNING, '
'WORKFLOW).')
parser.add_argument('--elev-bands', action='store_true',
help='compute the flowlines based on the Huss & Farinotti '
'2012 method.')
parser.add_argument('--centerlines', action='store_true',
help='compute the flowlines based on the OGGM '
'centerline(s) method.')
parser.add_argument('--skip-inversion', action='store_true',
help='do not run the inversion (level 3 files). '
'this is a temporary workaround for workflows '
'that wont run that far into level 3.')
parser.add_argument('--mb-model-class', type=str, default='MonthlyTIModel',
help='the mass balance model class to use. Options are '
'MonthlyTIModel (default) or SfcTypeTIModel.')
parser.add_argument('--inversion-volume-dataset', type=str,
default='iceboost',
choices=['iceboost', 'consensus'],
help="reference volume dataset to calibrate the ice "
"thickness inversion against. 'iceboost' (default, "
"IceBoost v2, RGI62/RGI70G/RGI70C) or 'consensus' "
"(Farinotti et al. 2019, RGI62 only).")
parser.add_argument('--mb-calibration-strategy', type=str,
default='informed_threestep',
choices=['informed_threestep', 'melt_temp',
'temp_melt'],
help='how to calibrate the massbalance. Currently one '
'of informed_threestep (default), melt_temp '
'or temp_melt.')
parser.add_argument('--dem-source', type=str, default='',
help='which DEM source to use. Possible options are '
'the name of a specific DEM (e.g. RAMP, SRTM...) '
'or ALL, in which case all available DEMs will '
'be processed and adjoined with a suffix at the '
'end of the file name. The ALL option is only '
'compatible with level 1 folders, after which '
'the processing will stop. The default is to use '
'the default OGGM DEM.')
parser.add_argument('--select-source-from-dir', type=str,
default=None,
help='if starting from a level 1 "ALL" or "STANDARD" DEM '
'sources directory, select the chosen DEM source here. '
'If you set it to "BY_RES" here, COPDEM will be used and '
'its resolution chosen based on the gdirs map resolution '
'(COPDEM30 for dx < 60 m, COPDEM90 elsewhere).')
parser.add_argument('--keep-dem-folders', action='store_true',
help='if `select_source_from_dir` is used, wether to keep '
'the original DEM folders in or not.')
parser.add_argument('--add-consensus-thickness', action='store_true',
help='adds (reprojects) the consensus thickness '
'estimates to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-itslive-velocity', action='store_true',
help='adds (reprojects) the ITS_LIVE velocity '
'estimates to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-millan-thickness', action='store_true',
help='adds (reprojects) the millan thickness '
'estimates to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-millan-velocity', action='store_true',
help='adds (reprojects) the millan velocity '
'estimates to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-hugonnet-dhdt', action='store_true',
help='adds (reprojects) the hugonnet dhdt '
'maps to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-bedmachine', action='store_true',
help='adds (reprojects) the Bedmachine ice thickness '
'maps to the glacier directories. '
'With --elev-bands, the data will also be '
'binned.')
parser.add_argument('--add-glathida', action='store_true',
help='adds (reprojects) the glathida point thickness '
'observations to the glacier directories. '
'The data points are stored as csv.')
parser.add_argument('--custom-climate-task', type=str, default=None,
help='Custom climate task import path in the form module:function. '
'If provided, it replaces the default process_climate_data.')
parser.add_argument('--custom-climate-task-kwargs', type=json.loads, default=None,
help='JSON dict of kwargs passed to the custom climate task.')
parser.add_argument('--add-distributed-thickness', action='store_true',
help='adds a thickness field to gridded_data using '
'distribute_thickness_per_altitude.')
parser.add_argument('--add-export-thickness-geotiff', action='store_true',
help='exports the distributed thickness field to '
'GeoTIFF files in a subfolder of the L3 summary '
'directory. Requires --add-distributed-thickness.')
parser.add_argument('--compute-hypsometry', action='store_true',
help='Compute the hypsometry tables for all glaciers, '
'added to the glacier directory and compiled in '
'the summary folder')
parser.add_argument('--test', action='store_true',
help='if you want to do a test on a couple of '
'glaciers first.')
parser.add_argument('--test-ids', nargs='+',
help='if --test, specify the RGI ids to run separated '
'by a space (default: 4 randomly selected).')
parser.add_argument('--rgi-file', type=str, default=None,
help='path to an RGI shapefile to use instead of '
'the default RGI region file.')
parser.add_argument('--intersects-file', type=str, default=None,
help='path to an intersects shapefile to use instead '
'of the default RGI intersects file.')
parser.add_argument('--disable-mp', action='store_true',
help='if you want to disable multiprocessing.')
parser.add_argument('--dynamic-spinup', type=str, default='',
help="include a dynamic spinup for matching glacier area "
"('area/dmdtda') OR volume ('volume/dmdtda') at "
"the RGI-date, AND mass-change from Hugonnet "
"in the period 2000-2020 (dynamic melt_f "
"calibration).")
parser.add_argument('--ref-mb-err-scaling-factor', type=float, default=0.2,
help="scaling factor to account for correlated "
"uncertainties of geodetic mass balance "
"observations when looking at regional scale. "
"Should be smaller or equal to 1.")
parser.add_argument('--dynamic-spinup-start-year', type=int, default=1979,
help="if --dynamic-spinup is set, define the starting"
"year for the simulation. The default is 1979, "
"unless the climate data starts later.")
parser.add_argument('--dynamic-spinup-extra-years-to-try', nargs='*',
default=[10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
help="if --dynamic-spinup is set, define additional "
"years to start the spinup BEFORE "
"--dynamic-spinup-start-year, tried as a last "
"resort if all other spinup periods failed (e.g. "
"'10 20' first tries to start 10 years before "
"--dynamic-spinup-start-year, and then 20 years "
"before, so the longest spinup is tried last). "
"Start years before the start of the climate data "
"are clipped to it. If you do not want to use it "
"set '--dynamic-spinup-extra-years-to-try none' "
"in the terminal.")
parser.add_argument('--dynamic-spinup-no-shorter-periods',
action='store_true',
help="if --dynamic-spinup is set, prevent the dynamic "
"spinup from starting AFTER "
"--dynamic-spinup-start-year. Per default, if the "
"spinup at --dynamic-spinup-start-year failed, "
"shorter spinup periods are tried first (down to "
"the start year of the geodetic mass balance "
"period).")
parser.add_argument('--geodetic-mb-file-path', type=str, default=None,
help='optional path or URL to a custom geodetic MB '
'file passed to MB calibration.')
parser.add_argument('--temp-bias-file-path', type=str, default=None,
help='path or URL to the temperature-bias prior file '
'passed to MB calibration. Required by the '
'informed_threestep strategy (and unused '
'otherwise): there is no default, the file has to '
'match the setup it is used with. It is created '
'with --temp-bias-run and the `oggm_temp_bias` '
'command.')
parser.add_argument('--temp-bias-run', action='store_true',
help='run the preprocessing needed to create the '
'temperature bias prior file. This forces '
'--max-level 3 and --skip-inversion, and writes '
'nothing but the L3 glacier statistics file. '
'Requires --mb-calibration-strategy temp_melt. '
'Feed the result to the `oggm_temp_bias` command '
'(together with the other regions) to create the '
'file.')
parser.add_argument('--store-fl-diagnostics', action='store_true',
help="Also compute and store flowline diagnostics during "
"preprocessing. This can increase data usage quite "
"a bit.")
parser.add_argument('--store-hydro-output', action='store_true',
help='Add optional hydrological model output')
parser.add_argument('--store-monthly-hydro', action='store_true',
help='Requires --store-hydro-output. Also store the '
'hydrological model output at monthly resolution. '
'This increases data usage quite a bit.')
parser.add_argument('--ref-area-yr', type=int, default=None,
help='Force the reference area used for the hydrological '
'output to the glacier state of the given simulation '
'year, instead of the largest area during the '
'simulation period.')
parser.add_argument('--override-params', type=json.loads, default=None)
args = parser.parse_args(args)
# Check input
rgi_reg = args.rgi_reg
if not rgi_reg:
rgi_reg = os.environ.get('OGGM_RGI_REG', None)
if rgi_reg is None:
raise InvalidParamsError('--rgi-reg is required!')
rgi_reg = '{:02}'.format(int(rgi_reg))
ok_regs = ['{:02}'.format(int(r)) for r in range(1, 20)]
if rgi_reg not in ok_regs:
raise InvalidParamsError('--rgi-reg should range from 01 to 19!')
rgi_version = args.rgi_version
border = args.map_border
if not border:
border = os.environ.get('OGGM_MAP_BORDER', None)
if border is None:
raise InvalidParamsError('--map-border is required!')
working_dir = args.working_dir
if not working_dir:
working_dir = os.environ.get('OGGM_WORKDIR', '')
output_folder = args.output
if not output_folder:
output_folder = os.environ.get('OGGM_OUTDIR', '')
border = int(border)
output_folder = os.path.abspath(output_folder)
working_dir = os.path.abspath(working_dir)
dynamic_spinup = False if args.dynamic_spinup == '' else args.dynamic_spinup
extra_years_to_try = args.dynamic_spinup_extra_years_to_try
if extra_years_to_try in [['none'], []]:
extra_years_to_try = None
else:
# argparse gives us strings if the user provided them in the terminal
try:
extra_years_to_try = [int(yr) for yr in extra_years_to_try]
except (TypeError, ValueError):
raise InvalidParamsError(
'--dynamic-spinup-extra-years-to-try takes years to start the '
'spinup before --dynamic-spinup-start-year, or the single '
f'value "none", but got {extra_years_to_try}!')
if any(yr <= 0 for yr in extra_years_to_try):
raise InvalidParamsError(
'--dynamic-spinup-extra-years-to-try must be positive (they '
'are counted backwards from --dynamic-spinup-start-year)!')
# All good
return dict(rgi_version=rgi_version, rgi_reg=rgi_reg,
border=border, output_folder=output_folder,
working_dir=working_dir, params_file=args.params_file,
is_test=args.test, test_ids=args.test_ids,
rgi_file=args.rgi_file,
intersects_file=args.intersects_file,
dem_source=args.dem_source,
start_level=args.start_level, start_base_url=args.start_base_url,
start_from_dir=args.start_from_dir,
max_level=args.max_level, disable_mp=args.disable_mp,
chunk_idx=args.chunk_idx, chunk_size=args.chunk_size,
glen_a_factor=args.inversion_glen_a_factor,
inversion_fs=args.inversion_fs,
logging_level=args.logging_level,
elev_bands=args.elev_bands,
skip_inversion=args.skip_inversion,
inversion_volume_dataset=args.inversion_volume_dataset,
centerlines=args.centerlines,
select_source_from_dir=args.select_source_from_dir,
keep_dem_folders=args.keep_dem_folders,
add_consensus_thickness=args.add_consensus_thickness,
add_millan_thickness=args.add_millan_thickness,
add_itslive_velocity=args.add_itslive_velocity,
add_millan_velocity=args.add_millan_velocity,
add_hugonnet_dhdt=args.add_hugonnet_dhdt,
add_bedmachine=args.add_bedmachine,
add_glathida=args.add_glathida,
add_distributed_thickness=args.add_distributed_thickness,
add_export_thickness_geotiff=args.add_export_thickness_geotiff,
compute_hypsometry=args.compute_hypsometry,
custom_climate_task=args.custom_climate_task,
custom_climate_task_kwargs=args.custom_climate_task_kwargs,
dynamic_spinup=dynamic_spinup,
ref_mb_err_scaling_factor=args.ref_mb_err_scaling_factor,
dynamic_spinup_start_year=args.dynamic_spinup_start_year,
dynamic_spinup_extra_years_to_try=extra_years_to_try,
dynamic_spinup_allow_shorter=(
not args.dynamic_spinup_no_shorter_periods),
mb_model_class=args.mb_model_class,
mb_calibration_strategy=args.mb_calibration_strategy,
geodetic_mb_file_path=args.geodetic_mb_file_path,
temp_bias_file_path=args.temp_bias_file_path,
temp_bias_run=args.temp_bias_run,
store_fl_diagnostics=args.store_fl_diagnostics,
store_hydro_output=args.store_hydro_output,
store_monthly_hydro=args.store_monthly_hydro,
ref_area_yr=args.ref_area_yr,
override_params=args.override_params,
)
def main():
"""Script entry point"""
run_prepro_levels(**parse_args(sys.argv[1:]))