Seeds ocean_sea_ice_t’s six category prognostics
(part_size/m_ice/m_snow/enth_ice/enth_snow/sal_ice) from
&ocean_ice_ic_nml, so a run can start with a live ice pack
instead of growing one from frazil (PLAN_PR24_ice_ic_path.md).
v1 ships two ANALYTIC seeding modes on top of the default no-op:
"zero" (default) — early return, touches nothing.
"uniform" — scalar concentration/thickness everywhere wet.
"latitudes" — SIS2 initialize_concentration_from_latitudes
(SIS_state_initialization.F90:489-531): a 0/1
step at arctic_edge/antarctic_edge off
metrics%geolatT. Defaults (+-91) => no ice.
Host-side setup code, called ONCE, before ocean_state_enter_data
(rdb_driver.F90, between configure_ocean_land_mask and
ocean_state_enter_data). No device kernel, no new state array,
zero do concurrent. A do concurrent here would either
round-trip the (unmapped) host arrays through the device once per
loop on -stdpar=gpu (measured 36.9 s regression elsewhere,
seed_h_layer_uniform_impl) or read stale device memory — see
rdb_ocean_state’s “Plain host loops, NOT do concurrent” precedent.
Mass, not thickness — and per-ICE-area, not per-cell, at ncat>1.
The prognostic is m_ice = ICE_RHO_ICE*h_ice [kg/m^2], and at
ncat>1 it is per unit ICE-COVERED area (SIS2 mH_ice) — NOT
multiplied by conc (module docstring of rdb_ice_state,
“:65-86”). At ncat==1 (legacy lumped mode) m_ice is per unit
CELL area instead, and part_size is left untouched
(part_size(:,:,0)=1 forever — the frozen PR 3b/3c contract).
Category allocation is an ITD fixed point, by construction. The
pack’s mass is binned directly against the already-computed
ice%mh_lim(1:ncat+1) (filled at slot init by
ice_itd_category_bounds) via ice_ic_target_category — the SAME
bounds ice_adjust_categories (rdb_ice_itd) uses to restore the
ITD every thermo window. Consumers of the ITD MUST read
ice%mh_lim, never HLIM_DFLT_TABLE (that table is private to
rdb_ice_state) and never a re-derivation from ncat alone — PR-58
may override mh_lim’s VALUES via &ocean_ice_nml hlim, and this
module’s IC must stay consistent with whatever init actually
computed. ice_adjust_categories applied to an IC-seeded state is
therefore a bit-exact no-op (test oracle, not a call site — see
“What NOT to take” below).
Enthalpy, not temperature.enth_ice/enth_snow are set from
the namelist ice temperature t_ice through the EXACT
ice_enth_from_ts inversion (rdb_ice_enthalpy), never assigned
directly — the thermodynamic prognostic is specific enthalpy
(J/kg), never temperature. sal_ice = s_ice; snow is fresh
(ice_enth_from_ts(t_ice, 0.0)).
k-convention.enth_ice/sal_ice are (..., nk_ice) BOTTOM-UP
(k=1 = ice bottom/ocean side, k=nk_ice = ice top/atm side) — the
opposite of rdb_ice_column’s internal top-down convention. A
uniform-t_ice IC is k-symmetric so this cannot be gotten wrong
here, but the first person to add a linear top-to-bottom
temperature profile must respect it.
What NOT to take from SIS2 (D-list, deliberate divergences):
D1 Do NOT call ice_adjust_categories (SIS2’s “seed into cat 1,
then adjust_ice_categories” flow) — it is a do concurrent
device kernel and the IC runs before enter_data. Bin
directly on the host against mh_lim; use the kernel only as
a TEST ORACLE.
D2 Do NOT seed a placeholder mass (mh_lim(c+1)) into empty
categories the way SIS2’s ice_state_mass_init does. Roundabout’s
live convention is m_ice = 0 for an empty category (what
ice_frazil_uptake_multicat_impl already produces, and what
the column entry gate keys on) — adopting SIS2’s placeholder
would run a full Winton solve on every empty category of every
cell for no change in the answer (part-weighted to zero).
D3 Do NOT port ICE_RELATIVE_TEMP_IC/ICE_RELATIVE_SALINITY/
spec_thermo_sal — v1 ships one bulk salinity + one bulk
temperature. Deferred.
D4 Do NOT port data_override / file-backed ICs — that is PR-14
(v1.1), explicitly out of scope here ("file" is deliberately
NOT in the conc_config allowed list).
"zero" (the default) is a stronger no-op than SIS2’s own IC path
(SIS2 sets the thermo fields unconditionally): it returns before
touching a single array, including sal_ice/enth_ice (already
sourced to ICE_BULK_SALINITY/0.0 by ocean_sea_ice_init) — so
the restart file bytes are unchanged too.
Nodes of different colours represent the following:
Solid arrows point from a submodule to the (sub)module which it is
descended from. Dashed arrows point from a module or program unit to
modules which it uses.
Where possible, edges connecting nodes are
given different colours to make them easier to distinguish in
large graphs.
Nodes of different colours represent the following:
Solid arrows point from a submodule to the (sub)module which it is
descended from. Dashed arrows point from a module or program unit to
modules which it uses.
Where possible, edges connecting nodes are
given different colours to make them easier to distinguish in
large graphs.
Variables
Type
Visibility
Attributes
Name
Initial
integer,
public,
parameter
::
ICE_IC_CONC_INVALID
=
-1
Sentinel for an unrecognised conc_config string (fail-loud
idiom — a pure function cannot error stop; validate_config
turns this into a logger%error).
Sea-ice IC scalar knob bundle (mirrors ice_evp_params_t).
Built ONCE from &ocean_ice_ic_nml via ice_ic_params_from_config;
every field a plain scalar (no derived-type deref inside the
host loop, same discipline as the EVP params — though this
module never runs on-device at all).
Ice thickness (m) where seeded — SIS2 ICE_INIT_MASS as a
thickness.
real(kind=wp),
public
::
h_snow
=
0.0_wp
Snow thickness (m) where seeded — SIS2 SNOW_INIT_MASS as a
thickness.
real(kind=wp),
public
::
s_ice
=
ICE_BULK_SALINITY
Ice bulk salinity (PSU) — SIS2 ICE_SALINITY_IC default,
which matches ocean_sea_ice_init’s own sal_ice source
value, so "uniform" at the default s_ice leaves sal_ice
unchanged.
real(kind=wp),
public
::
t_ice
=
-4.0_wp
Ice/snow temperature (degC) — SIS2 ICE_TEMPERATURE_IC
default.
Functions
public pure function ice_ic_params_from_config(conc_config, conc, h_ice, h_snow, t_ice, s_ice, arctic_edge, antarctic_edge) result(par)
Small constructor — build once from &ocean_ice_ic_nml. Copies
ice_evp_params_from_config’s shape; the one difference is
conc_config, a STRING here (parsed internally), because the
namelist knob is a nml_enum string, not an already-parsed
integer.
"zero"/"uniform"/"latitudes" -> ICE_IC_CONC_*; any other
string -> ICE_IC_CONC_INVALID (fail-loud idiom: a pure
function cannot abort, so validate_config V8 turns the
sentinel into a logger%error; nml_enum allowed= is the first
line of defence).
The IC’s target category for a pack of mass m [kg/m^2]
(per-ICE-area, ncat>1 convention): the top bin ncat is
UNBOUNDED above (mh_lim(ncat+1) is stored but never used as a
cap — ice_adjust_categories’s upward pass stops at c =
ncat-1), otherwise the unique c with
mh_lim(c) <= m < mh_lim(c+1). Declared ncat before the
explicit-shape mh_lim that uses it (decl-order).
Arguments
Type
Intent
Optional
Attributes
Name
real(kind=wp),
intent(in)
::
m
Pack mass (kg/m^2, per-ICE-area convention).
real(kind=wp),
intent(in)
::
mh_lim(ncat+1)
Category lower mass limits (kg/m^2), ice%mh_lim — NEVER a
re-derivation from ncat alone (PR-58 may override its
values via &ocean_ice_nml hlim).
integer,
intent(in)
::
ncat
Number of ice thickness categories.
Return Value
integer
Subroutines
public pure subroutine ice_init_apply(grid, ms, metrics, ice, par)
Outer shim (outer-shim + flat-impl pattern, mirrors
ice_adjust_categories). No-op when the ice slot is not live
(is_init gate) or when par%conc_config == ICE_IC_CONC_ZERO
(the default-off bit-identity contract — nothing is written, not
even sal_ice/enth_ice). ice is intent(inout) (not
out): h_lim/mh_lim were filled at init and must survive.