CryoSat2 - Model Comparison at Vatnajökull Ice Cap, Iceland#

This notebook was created at the beginning of the DTC Glaciers project for illustration of the scientific challenges to be tackled. We compare the output from the Open Global Glacier Model (OGGM) v1.6.1 against EO And in-situ observations at Vatnajökull ice cap, Iceland.

from oggm import utils, workflow, tasks, DEFAULT_BASE_URL, cfg
import yaml
import geopandas as gpd
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.dates as mdates

sns.set_style("whitegrid")
sns.set_context("notebook")
# Where to find the test files on EODS
dtcg_url = (
    "https://cluster.klima.uni-bremen.de/~dtcg/test_files/case_study_regions/iceland/"
)
# OGGM parameters
cfg.initialize(logging_level="ERROR")
cfg.PARAMS["use_multiprocessing"] = True
cfg.PARAMS["continue_on_error"] = True
cfg.PATHS["working_dir"] = utils.get_temp_dir("working_dir")
2026-02-24 16:47:28: oggm.cfg: Reading default parameters from the OGGM `params.cfg` configuration file.
2026-02-24 16:47:28: oggm.cfg: Multiprocessing switched OFF according to the parameter file.
2026-02-24 16:47:28: oggm.cfg: Multiprocessing: using all available processors (N=4)
2026-02-24 16:47:28: oggm.cfg: Multiprocessing switched ON after user settings.
2026-02-24 16:47:28: oggm.cfg: PARAMS['continue_on_error'] changed from `False` to `True`.

Load Vatnajökull RGI6 outlines#

# RGI6 ids
with open(
    utils.file_downloader(dtcg_url + "vatnajokull_rgi_ids.yml"), "r"
) as yaml_file:
    rgi_ids = yaml.safe_load(yaml_file)["rgi_ids"]

# Select the outlines from RGI6 file and convert to UTM
rgi_file = gpd.read_file(utils.get_rgi_region_file("06"))
rgi_file = rgi_file.loc[rgi_file.RGIId.isin(rgi_ids)].set_index("RGIId")
rgi_file = rgi_file.to_crs("EPSG:32628")
rgi_file.plot(ec="k");
../_images/7f9376123a29ecd7064006d1a337793f9bf864e37774284257a2db98ba472efe.png

Fetch the standard global OGGM runs#

gdirs = workflow.init_glacier_directories(
    rgi_ids,  # which glaciers?
    prepro_base_url=DEFAULT_BASE_URL,  # where to fetch the data?
    from_prepro_level=4,  # what kind of data?
    prepro_border=80,  # how big of a map?
)

Hide code cell output

2026-02-24 16:47:29: oggm.workflow: init_glacier_directories from prepro level 4 on 118 glaciers.
2026-02-24 16:47:29: oggm.workflow: Execute entity tasks [gdir_from_prepro] on 118 glaciers

Re-run OGGM at monthly timestep and load data#

workflow.execute_entity_task(
    tasks.run_from_climate_data,
    gdirs,
    init_model_filesuffix="_spinup_historical",
    init_model_yr=1979,
    store_monthly_step=True,
    ys=1979,
    ye=2020,
    mb_elev_feedback="monthly",
    output_filesuffix="_spinup_historical_monthly",
)

ds_monthly = utils.compile_run_output(
    gdirs, input_filesuffix="_spinup_historical_monthly"
)
2026-02-24 16:47:50: oggm.workflow: Execute entity tasks [run_from_climate_data] on 118 glaciers
2026-02-24 16:47:53: oggm.core.flowline: AttributeError occurred during task run_from_climate_data_spinup_historical_monthly on RGI60-06.00387: 'Dataset' object has no attribute 'time'
2026-02-24 16:48:12: oggm.utils: Applying global task compile_run_output on 118 glaciers
2026-02-24 16:48:12: oggm.utils: Applying compile_run_output on 118 gdirs.

Modelled mass change and comparison to calibration data#

OGGM#

years, months = utils.floatyear_to_date(ds_monthly.time)
df = pd.DataFrame(index=pd.to_datetime({"year": years, "month": months, "day": 1}))
df["OGGM_volume_m3"] = ds_monthly.sum(dim="rgi_id").volume.data
df["OGGM_mwe"] = (
    (df["OGGM_volume_m3"] - df["OGGM_volume_m3"].loc["2000-01"].values)
    / (rgi_file.Area.sum() * 1e6)
    * 900
    / 1000
)
f, ax = plt.subplots(figsize=(12, 7))
df["OGGM_mwe"].plot(ax=ax, label="OGGM v1.6.1")
ax.axhline(0, linestyle="--", color="black", linewidth=1)
plt.ylabel("m w.e.")
plt.title("Vatnajökull cumulative mass change 1979-2020 from OGGM")
plt.legend();
../_images/293d1cc1c7f583e98f197bdf89780ac7c267a8a7fbe25a0cebb105bcb42c13b8.png

Hugonnet observations#

ref_mb_df = utils.get_geodetic_mb_dataframe().loc[rgi_ids]
ref_mb_df = ref_mb_df.loc[ref_mb_df.period == "2000-01-01_2020-01-01"]
hugonnet_20yrs = (
    (ref_mb_df["area"] * ref_mb_df["dmdtda"]).sum() / ref_mb_df["area"].sum() * 20
)
hugonnet_20yrs_err = (
    (ref_mb_df["area"] * ref_mb_df["err_dmdtda"]).sum() / ref_mb_df["area"].sum() * 20
)
# Compute Hugonnet's mass balance estimates for the plot
p0 = df["OGGM_mwe"].loc["2000-01"].iloc[0]
p1 = hugonnet_20yrs + p0

f, ax = plt.subplots(figsize=(12, 7))
ptimes = df.loc[["2000-01", "2020-01"]].index
ax.errorbar(
    x=ptimes,
    y=[p0, p1],
    yerr=[0, hugonnet_20yrs_err],
    fmt="o",
    color="C3",
    label="Hugonnet et al. (2021)",
)
df["OGGM_mwe"].loc["1995":].plot(ax=ax, label="OGGM v1.6.1")
ax.axhline(0, linestyle="--", color="black", linewidth=1)
plt.ylabel("m w.e.")
plt.title("Vatnajökull cumulative mass change as calibrated")
plt.legend();
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pandas/plotting/_matplotlib/core.py:997: UserWarning: This axis already has a converter set and is updating to a potentially incompatible converter
  return ax.plot(*args, **kwds)
../_images/eb1a73e495ea0041de197ed0e0a2b7a153237e531d08239ce0c82851e063501e.png

Elevation change comparison#

Load Cryosat-2 data#

obs_file = utils.file_downloader(dtcg_url + "averaged_grid.csv")
df_obs_elev = pd.read_csv(obs_file, index_col=1, parse_dates=True)
df_obs_elev = df_obs_elev[
    "changes_t0"
]  # only keep average which is used for comparision

Hugonnet et al#

ref_mb_df = utils.get_geodetic_mb_dataframe().loc[rgi_ids]
ref_mb_df = ref_mb_df.loc[ref_mb_df.period == "2010-01-01_2020-01-01"]
# Hugonnet uses 850
hugonnet_10yrs = (
    (ref_mb_df["area"] * ref_mb_df["dmdtda"]).sum()
    / ref_mb_df["area"].sum()
    * 10
    * 1000
    / 850
)
hugonnet_10yrs_err = (
    (ref_mb_df["area"] * ref_mb_df["err_dmdtda"]).sum()
    / ref_mb_df["area"].sum()
    * 10
    * 1000
    / 850
)

WGMS#

dfw = pd.read_csv(utils.file_downloader(dtcg_url + "vatna_berthier.csv"), index_col=0)

dfwts = pd.DataFrame()

for y, d in dfw.iterrows():
    dfwts.loc[pd.Timestamp(year=y, month=4, day=1), "MB"] = d.bw
    dfwts.loc[pd.Timestamp(year=y, month=10, day=1), "MB"] = d.bs
    pass

dfwts["MB_m"] = dfwts["MB"] * 1000 / 850
dfwts["MB_m_cum"] = dfwts["MB_m"].cumsum()

Compute modelled elevation change#

# it is a bit unclear which basline was used by the observations
# from the meta_data: "average of the first 5 months (all data pre-2011)"
# baseline_volume = np.mean(volume_total.sel(time=slice(2011, 2011.5)))
# Her we just pick the first one in the obs timeseries
baseline_volume = df["OGGM_volume_m3"].loc["2010-09"].values

# divide by RGI area, which is the same area as used by the creation of the observation
rgi_area = np.sum([gdir.rgi_area_m2 for gdir in gdirs])

# finally the calculation of the modelled elevation change
# This assumes a density of 900 kg m2
df["OGGM_elev"] = (df["OGGM_volume_m3"] - baseline_volume) / rgi_area

Plot elevation change observed vs. modelled#

# Compute Hugonnet's mass balance estimates for the plot
p0 = df["OGGM_elev"].loc["2010-01"].iloc[0]
p1 = hugonnet_10yrs + p0

f, ax = plt.subplots(figsize=(12, 7))
ptimes = df.loc[["2010-01", "2020-01"]].index
ax.errorbar(
    x=ptimes,
    y=[p0, p1],
    yerr=[0, hugonnet_20yrs_err],
    fmt="o",
    color="C3",
    label="Hugonnet et al. (2021)",
)
df["OGGM_elev"].loc["2010":].plot(ax=ax, label="OGGM v1.6.1")
df_obs_elev.plot(ax=ax, label="CryoSat-2")

wgts = (dfwts["MB_m_cum"] - dfwts["MB_m_cum"].loc["2010-10-01"] - 1).loc["2010-10-01":]
wgts.plot(
    ax=ax,
    linestyle="--",
    color="grey",
    linewidth=0.5,
    alpha=0.3,
    marker="v",
    markersize=6,
    markerfacecolor="grey",
    markeredgecolor="k",
    label="In-situ obs",
)
ax.axhline(0, linestyle="--", color="black", linewidth=1)

# Set x-axis ticks to show every year
ax.xaxis.set_major_locator(mdates.YearLocator())  # One tick per year
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))  # Format as "YYYY"

plt.xlabel("")
plt.ylabel("m elevation")
plt.title("Vatnajökull cumulative elevation change")
plt.legend();
/opt/hostedtoolcache/Python/3.11.14/x64/lib/python3.11/site-packages/pandas/plotting/_matplotlib/core.py:997: UserWarning: This axis already has a converter set and is updating to a potentially incompatible converter
  return ax.plot(*args, **kwds)
../_images/8ca88e3e021b4d148801e806a8365e9efe4064e251a0961b73f063405631f934.png

WGMS comparison#

Load WGMS data#

fp_wgms = utils.file_downloader(dtcg_url + "WGMS_MB-DTC-Glaciers.csv")
wgms_data = pd.read_csv(fp_wgms)

Helper functions to access WGMS data

# conversions between wgms ids and rgi ids
fp_wgms_ids_conversion = utils.file_downloader(dtcg_url + "glacier_id_lut.csv")
df_wgms_ids = pd.read_csv(fp_wgms_ids_conversion)


def get_wgms_from_rgi(rgi_id):
    if rgi_id in df_wgms_ids["RGI60_ID"].values:
        return df_wgms_ids[df_wgms_ids["RGI60_ID"] == rgi_id]["WGMS_ID"].item()
    else:
        return None


def get_rgi_from_wgms(wgms_id):
    if wgms_id in df_wgms_ids["WGMS_ID"].values:
        return df_wgms_ids[df_wgms_ids["WGMS_ID"] == wgms_id]["RGI60_ID"].item()
    else:
        return None
def get_wgms_mb_observation(rgi_id):
    """
    Returns (mb_DataFrame, Glacier name)
    """
    wgms_id = get_wgms_from_rgi(rgi_id)

    # only not None if this glacier is a WGMS glacier
    if wgms_id:
        # check if we have some data
        if wgms_id in wgms_data["glacier_id"].values:
            wgms_glacier_data = wgms_data[wgms_data["glacier_id"] == wgms_id]
            df_wgms_glacier_data = pd.DataFrame(
                {"annual_balance": wgms_glacier_data["annual_balance"].values},
                index=wgms_glacier_data["year"].values,
            )
            return (
                df_wgms_glacier_data,
                wgms_glacier_data["glacier_id.short_name"].values[0],
            )

Compute hydro year averages#

# Here we need some code to work in hydro ye
df_annual = pd.DataFrame()
for y in range(1980, 2019):
    d0 = f"{y-1}-10-01"
    d1 = f"{y}-10-01"
    df_annual.loc[y, "OGGM_mwe"] = df.loc[d1]["OGGM_mwe"] - df.loc[d0]["OGGM_mwe"]

Plot WGMS and modelled mb for all available glaciers#

for gdir in gdirs:
    wgms_mb = get_wgms_mb_observation(gdir.rgi_id)
    if wgms_mb is not None:
        fig, ax = plt.subplots(1, 1)
        # add modelled smb
        df_annual["OGGM_mwe"].plot(ax=ax, label="OGGM v1.6.1")
        # add observation
        wgms_mb[0]["annual_balance"].plot(
            ax=ax, marker=".", label="Observations (WGMS)"
        )
        ax.set_title(f"{gdir.rgi_id}, {wgms_mb[1]}")
        ax.set_xlabel("")
        ax.set_ylabel("m w.e.")
        ax.legend()
        ax.set_xlim(1998, 2024)
        plt.show()
../_images/f8926755be405c2917fb357c11cc6321eea530205010588755a7061df9e5efee.png ../_images/20ff0ee6379fbb508d99f6e90e130b87868324ad7dd72b2a28851b3f6a32f8a4.png ../_images/8c23c197218e3c28b97f876a660a98980223b96211111316b237fe7ffffb1102.png ../_images/d1fb48bc2993d6c5505e8b4a3bea256fdabb3bbc581551c8a2e1b62c5af5da00.png ../_images/1b8a32c37ef059c8d8637e5a749597096b79aa010f4f4afe5eede860946ef89b.png ../_images/b04879662ef7e24b0044217f52643ea9be2e67c5bcbfb5861eb065ef4e56657c.png