Skip to content

Create 3d Greenland thermal forcing from ISMIP7-provided 2d and EN4 data - #979

Open
trhille wants to merge 12 commits into
MPAS-Dev:mainfrom
trhille:landice/create_3d_ismip7_greenland_tf
Open

Create 3d Greenland thermal forcing from ISMIP7-provided 2d and EN4 data#979
trhille wants to merge 12 commits into
MPAS-Dev:mainfrom
trhille:landice/create_3d_ismip7_greenland_tf

Conversation

@trhille

@trhille trhille commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Create 3d Greenland thermal forcing from ISMIP7-provided 2d and EN4 data. Note that this is currently branched from #978. We will rebase once that is merged.

Checklist

  • User's Guide has been updated
  • Developer's Guide has been updated
  • API documentation in the Developer's Guide (api.rst) has any new or modified class, method and/or functions listed
  • Documentation has been built locally and changes look as expected
  • The E3SM-Project submodule has been updated with relevant E3SM changes
  • The MALI-Dev submodule has been updated with relevant MALI changes
  • Document (in a comment titled Testing in this PR) any testing that was used to verify the changes
  • New tests have been added to a test suite

Handle the OCX scenario in the ismip7_forcing test group. OCX has no distinct
ESM model: it uses RACMO2.3p2-ERA (atmosphere) and EN4 (ocean) at data version
v1, with a named grid resolution (ocean-1000m) in the ocean file names.

get_params() now applies OCX-specific overrides (sources, versions, ocean grid
token) when scenario == OCX, so the [ismip7] model option is ignored and a
single config file drives both the atmosphere and ocean_thermal test cases.
Adds ismip7_forcing_ocx_gis.cfg and updates the user and developer docs.
Two fixes that block processing of forcing whose remapped files or source
directories differ from the CESM test data:

- Drop the ncremap-mangled 'time_bnds' variable when combining remapped
  files. ncremap collides the bounds 'nv' dimension with the destination
  mesh corner dimension and pads with a fill value that overflows time
  decoding. The steps build their own xtime and never use time_bnds.

- Skip input files whose trailing token is not a year (e.g. climatology
  averages like *_1978-2007_avg.nc) instead of crashing on int('avg').
Restructure processed-forcing output from
{output_base_path}/atmosphere_forcing/{model}_{scenario}/ (and the analogous
ocean_thermal_forcing path) to {output_base_path}/{group}/atmosphere/ and
{output_base_path}/{group}/ocean_thermal_forcing/, matching the directory
layout the ismip7_run test group globs for. 'group' is {model}_{scenario} for
the ESM scenarios and {scenario} (i.e. OCX) for OCX, whose atmosphere and
ocean sources differ but must share one directory. Documents the layout and
how to point ismip7_run's forcing_basepath / ocx_forcing_path at it.
…lation helper

process_smb.py, process_temperature.py, process_smb_gradient.py, and
process_temperature_gradient.py remapped the raw source file directly with
ncremap, unlike process_runoff.py, ocean_thermal, and the fracture pathway,
which all extrapolate fill/missing values on the source grid first. Without
that step, no-data cells near the ice-sheet margin (e.g. ocean cells in the
RACMO OCX source) leak the ~9.97e36 netCDF fill sentinel into valid
neighboring cells during conservative/bilinear remapping.

Add the same pre-remap extrapolation to the four affected steps.

Also promote the previously-duplicated extrapolate_source() (from
fracture/remap_utils.py, formerly re-implemented again in
ocean_thermal/process_thermal_forcing.py and process_runoff.py) to a single
shared compass.landice.tests.ismip7_forcing.remap_utils module, and update
all five call sites to use it instead of per-file private copies.
extrapolate_source moved from fracture.remap_utils to the shared
compass.landice.tests.ismip7_forcing.remap_utils module (828699b). Update
the api.rst autosummary entry accordingly; this was breaking the strict
Sphinx build (autosummary failed to import the moved symbol).
Port the standalone greenland_thermal_forcing tool into a new optional step
of the ocean_thermal test case. build_3d_thermal_forcing (GrIS only, gated by
process_ocean_thermal_3d) converts the 2-D GrIS thermal forcing into a
30-level 3-D field for MALI's nonlocal (Jourdain et al. 2020) melt scheme:
seven regional EN4 vertical profiles, per-cell seafloor anchoring to the 2-D
forcing, and per-region deltaT calibration. It auto-chains from the 2-D output
the same run just produced and writes ismip6shelfMelt_3dThermalForcing (plus
deltaT/gamma0/zOcean/basin), supplementing the 2-D file.

The ported science lives in ocean_thermal/greenland_3d.py; the 3-D-specific
parameters are supplied via a JSON config (config_file), while mesh, 2-D
forcing, output, and diagnostics paths are injected from the compass config.

Also standardize ocean scenario output names to 2dThermalForcing (GrIS) /
3dThermalForcing (AIS) and update the ismip7_run tf globs accordingly (AIS
ingests 3-D, GrIS 2-D). Adds docs and no new dependencies (shapely/h5py/dask
already present).

Not yet wired: ismip7_run GrIS streams to ingest the 3-D field (follow-up).
Add use_3d_thermal_forcing (default false) to [ismip7_run_gris]. When true,
ismip7_gris looks for *3dThermalForcing_*.nc instead of *2dThermalForcing_*.nc,
reads ismip6shelfMelt_3dThermalForcing + ismip6shelfMelt_zOcean at annual
intervals (instead of ismip6_2dThermalForcing at monthly intervals), adds an
ismip7_params stream reading ismip6shelfMelt_deltaT/_basin/_gamma0 from
melt_params_path (mirroring the AIS convention, and consuming the output of
the new build_3d_thermal_forcing step in landice/ismip7_forcing/ocean_thermal),
and sets config_use_3d_thermal_forcing_for_face_melt = .true. melt_params_path
is now validated as required when use_3d_thermal_forcing is true.

streams.landice.template gates the TF stream's variable list and the new
ismip7_params stream with Jinja2 conditionals on use_3d_thermal_forcing;
verified both branches render as valid XML with the expected variables.

Updates user and developer docs.
…SON config

- _find_variable() returned dataset.variables[name] (a low-level xr.Variable),
  not a full DataArray. Variable.isel() doesn't accept drop=True, so passing a
  positional indexer dict together with drop=True raised 'cannot specify both
  keyword and positional arguments to .isel'. Return dataset[name] instead.

- calibrate_regional_delta_t raised when a region had zero initially-floating
  cells, aborting the whole calibration. Skip such regions instead: print a
  diagnostic, leave their monthly means as NaN, set deltaT=0, and leave
  achieved melt as NaN (undefined, since there's no floating ice to melt).

Also add greenland_3d_tf_config.json, a real example JSON config for the
build_3d_thermal_forcing step (region-mask/EN4/GeoJSON paths only; mesh,
forcing_2d, output, and diagnostics are always injected by the step), and
wire it into ismip7_forcing_ocx_gis.cfg via process_ocean_thermal_3d and
[ismip7_ocean_thermal_3d] config_file.
scipy's classic/CDF-2 (NETCDF3_64BIT) writer produces a header that
netCDF-C (ncdump, MALI) rejects with "Unknown file format" when a
dataset mixes a record (unlimited-Time) variable with a scalar (0-D)
variable, as in this output (ismip6shelfMelt_gamma0 alongside xtime
and ismip6shelfMelt_3dThermalForcing). scipy can read its own broken
file back, masking the problem. Switch to engine="netcdf4", which
writes a conformant CDF-2 file and still streams record-by-record.
deltaT/gamma0 are calibrated once against OCX and must be held fixed
for every ESM scenario (recalibrating per-ESM would force all their
historical mean melt rates toward the same targets and erase the
differences between ESM ocean forcings). Config gains scenario and
melt_params_file; Config.calibrate_delta_t gates calibration to
scenario == OCX. Non-OCX runs load deltaT/gamma0/basin from an
existing melt_params_file instead of recalibrating, validating that
the stored basin layout and gamma0 match.

Also split ismip6shelfMelt_basin/gamma0/deltaT out of the time-varying
3-D thermal forcing output into their own file (write_melt_params/
read_melt_params), independent of ismip6shelfMelt_3dThermalForcing.
This matches how ismip7_run already expects a separate melt_params_path
file (see ismip7_gris streams.landice.template ismip7_params stream).

build_3d_thermal_forcing.py always points melt_params_file at the OCX
output directory, regardless of which scenario is currently being
processed.
The xtime char array from the 2-D forcing was carried over and run
through xarray's character coder a second time, splitting each byte
into a spurious length-1 dimension. Build xtime fresh from the decoded
timestamps following the existing compass idiom (ljust(64) strings ->
dtype 'S') and let xarray encode the StrLen char dimension, so xtime
is written as a proper char(Time, StrLen) variable.
A 285-year monthly 3D forcing field would be ~113 GB in a single file.
Write the output in blocks of output_years_per_file years (default 10),
one file per block named by its block start year (..._<YYYY>.nc), which
the run side addresses with a $Y filename_template plus a
filename_interval.

Also correct the GrIS 3D thermal-forcing cadence to monthly on the run
side; the annual filename cadence only applies to Antarctica, whose
ISMIP7 3D forcing is provided annually.

- greenland_3d.py: add Config.output_years_per_file; year_chunks and
  chunk_output_path helpers; write one file per block via
  _write_forcing_chunk.
- greenland_3d_tf_config.json: add output_years_per_file example.
- ismip7_gris/set_up_experiment.py: symlink the whole chunk series,
  derive filename_interval and reference_time, force monthly TF.
- ismip7_gris/streams.landice.template: templated TF filename_interval
  and reference_time.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant