Port of add_frazil_SIS2 (SIS2_ice_thm.F90:1342-1444, Apache-2.0),
bulk-salinity mode only. Spends the WHOLE per-cell frazil bank
(ocean_sea_ice_t%frazil_heat, banked by rdb_ice_frazil’s
surface-freezing clamp) once per thermo step by depositing new ice
mass, evenly split across the nk_ice layers of category 1
(cat = 1 — the only category this v1 model fills; part_size is
untouched, same convention as PR 3a).
TRAP #1 (module docstring precedent: rdb_ice_column,
rdb_ice_mass%ice_bottom_freeze): the ocean-water specific
enthalpy consumed by the freeze is the LIQUID form
enth_ocean = ice_enthalpy_liquid(sst, s_surf), never the
frozen/mushy ice_enth_from_ts — see ice_frazil_uptake_column.
Index-flip discipline (PR 3a TRAP #2 precedent): the state
arrays enth_ice/sal_ice are BOTTOM-UP (k=1 = ice bottom); the
SIS2 algorithm spends the bank TOP-DOWN. The flip happens ONLY at
the gather/scatter boundary inside ice_frazil_uptake_column —
nothing in the per-layer spend loop ever sees the bottom-up
convention.
Two-mode convention (PR 4a, rdb_ice_state module docstring):
ncat == 1 — legacy lumped mode, dispatched to the EXISTING
ice_frazil_uptake_impl byte-for-byte UNCHANGED: ALL frazil ice
deposits into category 1, treated as per unit CELL area (kg per m²
of cell) — self-consistent with the frazil bank (J per m² of cell)
and the brine-rejection flux. ncat > 1 — SIS2 ITD mode, dispatched
to ice_frazil_uptake_multicat_impl: the bank ANNEXES open water
into the thinnest occupied-or-empty category (k_merge, SIS2
SIS_slow_thermo.F90:1121-1146) via an area-weighted dilution at
constant mass (part(k_merge) += part(0), thickness drops, mass
doesn’t — the SIS2 area-creation move), then spends the bank on
that category’s column PER UNIT ICE AREA
(frazil_col = frazil_heat/part(k_merge), SIS_slow_thermo.F90:
1181-1186) via the SAME UNCHANGED ice_frazil_uptake_column. SIS2’s
default SIS2_FILLING_FRAZIL=.true. thin-category-fill mode (which
would spread new ice across MULTIPLE thin categories instead of one
k_merge) is NOT ported — v1 always merges into a single category,
documented divergence.
Salt bookkeeping (Boussinesq virtual-flux convention): freezing
m_frozen kg/m² of seawater at salinity s_surf into ice that
keeps salt_to_ice = m_frozen * ICE_BULK_SALINITY yields a diag
rate salt_flux_diag = (m_frozen*s_surf - salt_to_ice)/dt_therm
[PSU·kg/m²/s]. The ocean’s water MASS is not reduced (volume-
conserving virtual-salt-flux ocean — MOM6/SIS2 default); the
closed-budget identity is exact by construction:
rho0*Delta(sum_k hTr_S) + salt_into_ice == m_frozen*s_surf per
cell (rho0 = sf%rho0; the surface-flux apply gives
Delta(hS) = Q_salt*dt_therm/rho0). Freshwater/mass coupling is
PR-3c+ territory. NOTE: when s_surf < ICE_BULK_SALINITY the
brine flux goes NEGATIVE (freezing freshens the ocean locally) —
this is SIS2-faithful and deliberately not clamped.
Energy accounting: Q_heat is NOT written by this module. The frazil latent heat was already credited to the ocean by the PR-1 surface clamp (which warmed the surface to T_f when it banked the deficit); spending the bank here as ice latent heat is the closing half of that exchange — writing a Q_heat here would double-count. Melt-side Q_heat/Q_salt arrive with PR 3c.
Mirrors the outer-shim + flat-impl + !$acc routine seq
column-worker structure of rdb_ice_frazil /
rdb_ice_column%ice_thermo_columns.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| real(kind=wp), | public, | parameter | :: | ICE_FRAZIL_T_OFFSET | = | 0.5_wp |
SIS2 |
Outer shim (outer-shim + flat-impl pattern): dereference the
tracer registry (ms%tracers(idx)%hTr) on the HOST and forward
bare arrays to the device kernel — NVHPC stdpar cannot follow
the array-of-derived-types indirection inside a do-concurrent
body (same rule as ice_frazil_accumulate). No-op when either
S or T is unregistered.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(eos_t), | intent(in) | :: | eos | |||
| type(multilayer_state_t), | intent(in) | :: | ms |
ocean-side effect of the uptake (brine rejection) arrives
only via |
||
| type(ocean_sea_ice_t), | intent(inout) | :: | ice | |||
| real(kind=wp), | intent(in) | :: | dt_therm |
Effective thermo timestep (s) — |
Per-(cell,category-1) column worker: gather + flip
(bottom-up -> top-down, TRAP #2 discipline), spend the whole
frazil bank evenly over the nk layers (SIS2:1402), rebalance,
scatter + flip back. Port of add_frazil_SIS2
(SIS2_ice_thm.F90:1342-1444), bulk-salinity mode
(salin_freeze = ICE_BULK_SALINITY, SIS_slow_thermo.F90:
1207-1209) — the ice_rel_salin mode is NOT ported.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nk |
Number of ice layers (declared first — decl-order). |
||
| real(kind=wp), | intent(in) | :: | frazil |
Whole per-cell frazil bank to spend this call (J/m² of cell). |
||
| real(kind=wp), | intent(in) | :: | tfw |
Seawater freezing temperature at the surface (degC). |
||
| real(kind=wp), | intent(in) | :: | sst |
Sea-surface temperature (degC) — feeds the TRAP-#1 liquid ocean enthalpy. |
||
| real(kind=wp), | intent(in) | :: | s_surf |
Sea-surface salinity (PSU) — feeds the TRAP-#1 liquid ocean
enthalpy (unused by the linear formula, kept for call-site
parity with |
||
| real(kind=wp), | intent(in) | :: | m_snow |
Snow mass per unit CELL area (kg/m²) — untouched, carried through only to seed the local column’s slot 0. |
||
| real(kind=wp), | intent(inout) | :: | m_ice_tot |
Total category-1 ice mass per unit CELL area (kg/m²); in = prior step, out = post-freeze. |
||
| real(kind=wp), | intent(inout) | :: | enth_ice_bu(nk) |
BOTTOM-UP ice specific enthalpies (J/kg) — state order,
|
||
| real(kind=wp), | intent(inout) | :: | sal_ice_bu(nk) |
BOTTOM-UP ice bulk salinities (PSU) — state order. |
||
| real(kind=wp), | intent(out) | :: | m_frozen |
Total new-ice mass formed this call (kg/m² of cell). |
||
| real(kind=wp), | intent(out) | :: | salt_to_ice |
Salt content retained by the new ice (kg/m² of cell) —
|
Device kernel over PHYSICAL cells (ghosts excluded — same
physical-cells-only contract as ice_frazil_accumulate_impl and
ice_thermo_columns). Per wet, non-vanished, banked cell:
sample SST/SSS at k = nz, compute the seawater freezing point,
spend the WHOLE bank on category-1’s column via
ice_frazil_uptake_column, and reset frazil_heat to 0 (fully
spent). The per-cell diags (m_frozen_diag, salt_flux_diag)
are zeroed UNCONDITIONALLY first, then overwritten under the
gate — an unbanked/dry/land/vanished cell reports zero, not a
stale value from a prior window.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | hTr_T(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | hTr_S(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| type(eos_t), | intent(in) | :: | eos | |||
| real(kind=wp), | intent(inout) | :: | frazil_heat(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | m_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | m_snow(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | enth_ice(nx,ny,ncat,nk) | |||
| real(kind=wp), | intent(inout) | :: | sal_ice(nx,ny,ncat,nk) | |||
| real(kind=wp), | intent(inout) | :: | m_frozen_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | salt_flux_diag(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | dt_therm | |||
| integer, | intent(in) | :: | nghost | |||
| integer, | intent(in) | :: | ncat | |||
| integer, | intent(in) | :: | nk | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny |
ncat>1 SIS2 ITD-mode frazil spend. Port of SIS2
SIS_slow_thermo.F90:1121-1146 + 1181-1186 (non-filling mode —
SIS2’s default SIS2_FILLING_FRAZIL=.true. thin-category fill is
DEFERRED, see module docstring). Per banked cell (same
wet/non-vanished/bank>0 gate as ice_frazil_uptake_impl):
1. k_merge scan (SIS2:1124-1129): first category c with
part(0) + part(c) > 0.01; falls back to k_merge = 1 if
no category qualifies (SIS2’s k_merge default).
2. Open-water annexation (SIS2:1131-1145): if part(0) > 0,
dilute category k_merge’s thickness at CONSTANT MASS —
m_ice(k_merge)/m_snow(k_merge) scale by
part(k_merge)/(part(k_merge)+part(0)), part(k_merge)
absorbs all of part(0), part(0) -> 0. enth/sal are
per-MASS intensive — untouched by an area-only dilution.
3. Per-ice-area spend (SIS2:1181-1186):
frazil_col = frazil_heat/part(k_merge) (J per m² of
category area; the denominator is > 0 by construction —
either an occupied category was found, or step 2 just grew
part(k_merge) from the Σpart=1 invariant), then the SAME
UNCHANGED ice_frazil_uptake_column spends it on category
k_merge’s column.
4. Diags (per cell, part-weighted back to CELL-area units to
match the ncat==1 diag convention that the couplers and
rdb_ice_thermo_driver consume): m_frozen_diag =
part(k_merge)*m_frozen_pt; salt_flux_diag =
part(k_merge)*(m_frozen_pt*s_surf - salt_to_ice_pt)/dt_therm;
frazil_heat reset to 0 (fully spent). Both diags are
zeroed UNCONDITIONALLY at loop top, same contract as
ice_frazil_uptake_impl.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | hTr_T(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | hTr_S(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | h_layer(nx,ny,nz) | |||
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| type(eos_t), | intent(in) | :: | eos | |||
| real(kind=wp), | intent(inout) | :: | frazil_heat(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | part_size(nx,ny,0:ncat) | |||
| real(kind=wp), | intent(inout) | :: | m_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | m_snow(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | enth_ice(nx,ny,ncat,nk) | |||
| real(kind=wp), | intent(inout) | :: | sal_ice(nx,ny,ncat,nk) | |||
| real(kind=wp), | intent(inout) | :: | m_frozen_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | salt_flux_diag(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | dt_therm | |||
| integer, | intent(in) | :: | nghost | |||
| integer, | intent(in) | :: | ncat | |||
| integer, | intent(in) | :: | nk | |||
| integer, | intent(in) | :: | nz | |||
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny |