Skip to content

awiesm3: coupled-slab ice surface temperature, cavity hnode reset, iceberg bounds-check option - #966

Open
JanStreffing wants to merge 16 commits into
mainfrom
awiesm3-implicit-ice-surftemp
Open

awiesm3: coupled-slab ice surface temperature, cavity hnode reset, iceberg bounds-check option#966
JanStreffing wants to merge 16 commits into
mainfrom
awiesm3-implicit-ice-surftemp

Conversation

@JanStreffing

Copy link
Copy Markdown
Collaborator

The branch AWI-ESM3 develop-is is built from. Opening it for visibility and review, not to be merged yet: see the ordering note at the bottom.

What is in it that is not already on main

Already on main, listed so reviewers are not surprised to see the commits

#962, #963 and #964 all merged while this branch was in use, so the branch's own copies of them are redundant. The merge commit for #964 is still in the history here.

Do not merge this before the others

This must go after #960 and #961, which are the same changes as standalone PRs and should be reviewed on their own terms rather than swept in as part of a large branch. If #957 is the preferred home for the coupled-slab work, that should merge first too and this branch should be rebased onto it, leaving only the cavity hnode reset and the CMake option here.

Merging this first would land four separable changes in one commit and make the individual PRs harder to review or revert.

Validation

Runs coupled in AWI-ESM3 (FESOM2 CORE3 cavity + OIFS 48r1 + PISM), including across a mesh-change leg with icebergs: a full year with ib_num = 1883, 948 carried and 935 calved, completing normally.

The received atmosphere->ice flux a2ihf is frozen over the OASIS coupling
interval; the explicit skin relaxation therefore has no flux-temperature
feedback and its equilibrium t* = TFrezs + a2ihf*zsniced/con scales with the
snow/ice insulation. ice_surftemp now linearizes the flux around the anchor
tref (the ist actually transmitted to the atmosphere, captured at the OASIS
send into ice%atmcoupl%ist_ref):

    Q(t) = a2ihf + zlam*(tref - t),  zlam = 4*eps*sigma*tref^3 + zlam_turb

zlam_turb (16 W/m2/K) is the bulk near-neutral turbulent sensitivity
rho*cp*C_H*|U|; radiative + turbulent match the ~20 W/m2/K flux derivative
used as the standard fallback in NEMO/SI3-family couplings (EC-Earth4's
exported A_dQns_dT is the radiative part only). Energy the linearization
adds/removes is repaid from latent heat via qlam in the ice growth budget
(Qatmice = -qres-qcon+qlam), which reduces to the ECHAM-branch -a2ihf in
steady state, so the atmosphere-booked flux is conserved exactly. The melt
residual qres includes zlam for consistent booking at the 273.15 K cap.

Also carries a temporary log-only ICEDBG diagnostic (received a2ihf + state
at t < 225 K) for the descent-episode investigation; strip before merging.
…d growth flux

Companion to the OIFS coupled-slab change (atmosphere solves its ice-tile
skin implicitly with slab conduction through the coupled FESOM ice state):

- new o2a field sit_feom = m_ice (effective grid-mean sea-ice thickness;
  nsend 7->8). The atmosphere divides by the received ice fraction for the
  per-ice thickness its slab conduction needs.
- ist send restored to the concentration-weighted convention (ist*a_ice),
  paired with ECE_CPL_NEMO_WEIGHTED_ICE; inert while the atmosphere owns the
  ice temperature (LNEMOLIMTEMP=.false.) but keeps the o2a blend
  representative if the ingest is ever re-enabled.
- new namelist.ice logical latm_owns_ist (default .false.): when the
  atmosphere owns the ice surface temperature, the ice growth budget takes
  the received atmosphere flux directly (Qatmice=-a2ihf, ECHAM convention)
  and FESIM's own surface solve is internal-only (albedo/melt-pond state).
- ice_surftemp: bulk near-neutral turbulent term added to the implicit flux
  linearization (total ~20 W/m2/K, the NEMO/SI3-family fallback derivative).
- temporary log-only ICEDBG descent diagnostic (strip before merging).
…_ale

Cavity columns and their open-ocean edge ring (ulevels_nod2D_max>1, the
same exclusion class as the per-step hnode<->hnode_new sync) keep layer
thicknesses fixed at the mesh nominal. A restart hnode deviating from
that nominal (e.g. remapped from a formerly ice-free zstar column after
a moving-cavity mesh change) persists forever, and the tracer T* update
turns the mismatch into a multiplicative content drain every step (both
tracers decay by (hnode_new-hnode)/hnode_new per step; validated with
per-operator probes). Reset hnode to hnode_new after the restart read
and rebuild the depth arrays.
The cold-blowup descent investigation these lines served is closed
(multi-year coupled spin-ups run clean below the polar-night range),
so drop the log-only diagnostic block and its line counter.
…ns-ist path

The atmosphere owns the ice surface temperature (OIFS coupled-slab mode)
in every production configuration, so the latm_owns_ist runtime switch and
the FESIM-owns-ist growth-flux convention it selected (Qatmice=-qres-qcon+qlam,
with the qlam latent-heat closure) are dead. Remove the switch, the dead
branch, and the qlam bookkeeping; Qatmice=-a2ihf unconditionally.

The dQ/dT (zlam/tref/ist_ref) linearization stays: ice_surftemp still runs
to set the internal skin temperature used for albedo/melt-pond state.
fer_solve_Gamma solves the GM streamfunction over
  nzmin = ulevels_nod2D_max(n), nzmax = nlevels_nod2D_min(n)
(max/min over the node's surrounding elements). When a node touches both a
deep cavity element and a shallow one these can invert (nzmin >= nzmax). Both
sweeps then run zero times, but "tr(:,nzmax) = tp(:,nzmax)" still executes and
publishes an unassigned element of the automatic array tp into fer_gamma.
Such a node carries no GM transport, so zero its column and skip.

Also guard two non-short-circuit .and. expressions that read index 0:
  ice_maEVP.F90  ulevels(edge_tri(2,ed)) at boundary edges (edge_tri(2,ed)==0)
  io_xios.F90    angles(k) at the end of each insertion-sort sweep

(cherry picked from commit 58ccb44)
'alb' (ice%atmcoupl%ice_alb) is registered as an output stream in io_meandata
and declared detect_missing_value="true" in field_def, but it is missing from
io_xios_is_ice_field. The sender therefore never writes _FillValue into
ice-free cells and the field falls through to the generic wet-point mask, so
XIOS time-means of sea-ice albedo include open water, where the quantity is
not defined.

Add 'alb' to the ice-field list alongside ist/a_ice and the rest.

(cherry picked from commit 93b8526)
An ice-sheet-coupled run legitimately reaches zero icebergs: a cold start
before the ice sheet has discharged anything, or a leg where it discharges
into no basin. Nothing in the iceberg code tests ib_num against zero, and
the stepping itself is fine with it -- the loops are do ib = 1, ib_num and
the work arrays are dimension(16*ib_num), both harmless when empty.

Only the diagnostic output is not. init_buoy_output does

  nf_def_dim(ncid, 'number_tracer', ib_num, dimid_ib)
  nf_def_dim(ncid, 'time', NF_UNLIMITED, dimid_rec)

and in classic netCDF a zero-size dimension is unlimited, so the second
call fails with NC_UNLIMITED size already in use and takes the run down at
step 1.

Guard the five call sites on ib_num > 0. Switching the file to NF_NETCDF4
would also fix it, since there zero-size dimensions are ordinary and
several unlimited ones are allowed, but that changes the output format for
everyone.
The guard that skips halo nodes puts both tests in one .or., where the
left test is meant to keep the right one from running. Fortran does not
work that way and ifort evaluates both sides, so ulevels_nod2d(0) is read
on exactly the case the guard exists to skip. Split it into two tests.
Brings:
  - cap_ibhf_n, capping iceberg-driven cooling at -1.85 C per cell/step,
    toggled by namelist icebergs%l_cap_ibhf_n (default .true.)
  - a guard marking a berg melted when iceberg_elem falls outside
    1..elem2D, instead of the par_ex+stop that hangs every rank
  - trajectory() skipped only when l_allowgrounding==2 AND the berg is
    actually grounded, not globally
  - cos(lat) floor in trajectory() against divide-by-zero at high latitudes
It was defaulted ON to chase the corrupted iceberg_elem, and it did its
job: it caught the ulevels_nod2d(0) read within 100 timesteps, now
#963. That hunt is over, and -check bounds at -O0 on the code
that runs per berg per step is not something a production leg should pay
for. Still available with -DFESOM_ICB_BOUNDS_CHECK=ON.
local_idx_of is saved but was allocated at each of the five iceberg dump
sites and never deallocated, so the second node to blow up on a rank hit
"forrtl: severe (151): allocatable array is already allocated" inside the
reporter. The run then died on the diagnostic rather than on the blowup,
and the message naming the node and the failing check was never reached.

Only reachable with use_icebergs, which is why it sat unnoticed since the
sites were added a year ago.
global2local zeroes every element the rank does not own, so for an iceberg
elsewhere in the domain local_idx_of(iceberg_elem(ib)) is 0 and the next
line indexes elem2d_nodes at 0. With icebergs spread over all ranks that is
almost every iceberg on almost every rank, so the dump segfaults as soon as
it reaches the loop.

It never reached it before: the allocate above aborted first. Same shape as
8aaca2d, which stopped ulevels_nod2d being indexed with a halo node marked 0.
@JanStreffing JanStreffing added this to the FESOM 2.8 milestone Aug 18, 2026
Brings in the 3-equation cavity melt fix (#875), without which
cavity_heat_water_fluxes_3eq takes sqrt() of a negative discriminant and fills
every ice-shelf column with NaN. On the corrected CORE3 mesh that killed a
coupled leg at mstep=1 on 121 ranks, all at Antarctic cavity nodes, while the
same mesh ran a full standalone year on a FESOM that had the fix.

Also brings in 501a808, which drops the obsolete per-tracer _AB restart
variables, so restarts written by current main are readable here.

One conflict, in gen_forcing_couple.F90 after cpl_oasis3mct_send: this branch
adds the __oifs ist_ref anchor (#957) and main adds the __recom/__usetp endif,
and both sides opened a block sharing one #endif. Resolved by keeping both,
each with its own guard.
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