Port of ice_temp_SIS2 + laytemp_SIS2 + update_lay_enth
(SIS2_ice_thm.F90:169-945, Apache-2.0) under the ICE_CP_BRINE ==
ICE_CP_ICE simplification (rdb_ice_enthalpy module docstring):
every per-layer implicit solve and T<->E inversion is a
closed-form quadratic — no Newton / false-position iteration
anywhere. Ported line-by-line from the validated stdlib-only
Python prototype tmp_local_artifacts/ice_pr3a_prototype/
sis2_column.py; when any formula here and SIS2 itself seem to
disagree, the prototype (which reproduces SIS2’s own commented-out
col_check energy-closure diagnostic to ~1e-14 fractional) is
the tiebreaker.
Index convention — TRAP #2. Internal columns are TOP-DOWN,
identical to SIS2 and the prototype: index 0 = snow, 1..nk =
ice, top to bottom. Roundabout’s state (ocean_sea_ice_t) is
BOTTOM-UP: enth_ice(..., 1) = ice bottom (ocean side),
enth_ice(..., nk_ice) = ice top (atm/snow side). The flip
happens ONLY at the gather/scatter boundary in ice_column_step
(local(k) = state(nk+1-k), the kg = nz+1-k idiom of
kappa_shear_column_driver, rdb_ocean_kappa_shear.F90:366) —
nothing inside ice_temp_sis2 / laytemp_sis2 / update_lay_enth
ever sees the bottom-up convention.
Ocean-freeze enthalpy — TRAP #1. The bottom-freeze ocean-side
enthalpy is the LIQUID formula ice_enthalpy_liquid(sst, s_surf)
(SIS_slow_thermo.F90:981), never the frozen/mushy
ice_enth_from_ts(tfw, sice) — the latter is ~8-9x more negative
at typical sea-ice salinities, inflating freeze mass per Joule and
making Stefan growth ~3x too fast. Lands in ice_column_step
step 4 (§3.4).
fb is a post-hoc residual, not a matrix BC — TRAP #3. The
conduction matrix’s bottom row always couples to the freezing
temperature tfw (cc(nk+1) = 2*kk*dtt); fb (ocean->ice heat
flux) enters exactly once, AFTER the conservative enthalpy update,
as bmelt = bmelt + (dtt*fb - tflux_bot). See ice_temp_sis2.
bb(k) two-branch live formula — TRAP #4. Not dead code: see
ice_temp_sis2’s bb computation, the (Cp_brine - Cp_ice) term
kept explicit even though it is 0 under the simplification
(matches the prototype’s chosen style, sis2_column.py:214).
nk_ice == 2 quasi-conservative double-pass — TRAP #5. After
the up/down tridiagonal estimate, every layer temperature is
re-solved via laytemp_sis2 (SIS2_ice_thm.F90:372-388) — this is
what pulls the Stefan-problem error under 1%; see ice_temp_sis2.
ICE_CP_BRINE == ICE_CP_ICE is asserted at slot init
(ocean_sea_ice_init, rdb_ice_state) so the unported
Newton/false-position branches (SIS2_ice_thm.F90:625-692,
837-870, 1876-1933) are provably unreachable.
Snow lands AFTER optics + conduction — TRAP #6 (PR 26).
ice_column_step’s snow argument is added in step 4 (resize,
via ice_snow_accumulate), which runs strictly after step 2
(optics, ice_optics_csim4) and step 3 (conduction,
ice_temp_sis2) already used the PRE-snowfall m_snow. This is
SIS2’s fast/slow split (ice_resize_SIS2 runs after the
conduction solve, SIS2_ice_thm.F90:1122), NOT an oversight: new
snow IS meltable in the same window (ice_top_melt_peel starts
at k=0), but its albedo and conduction effect are felt only on
the NEXT window. Do not move the add earlier chasing “why doesn’t
the albedo respond immediately”.
Flood after melt, before rebalance — TRAP #7 (PR 27). The
Archimedes freeboard snow-ice flood (ice_snow_ice_flood) runs
AFTER ice_bottom_melt_peel (so freshly-converted mass is not
re-melted this step and m_i reflects the post-melt column) and
BEFORE ice_rebalance_layers (else the new mass in layer 1 is
never redistributed across the nk layers) — exactly SIS2’s order
(ice_resize_SIS2 -> rebalance_ice_layers,
SIS_slow_thermo.F90:998,1008). It writes local index 1 (the
TOP ice layer, TRAP #2) — do not “helpfully” index nk.
Everything pure; every per-column routine !$acc routine seq;
explicit-shape dummies with integer dims declared before the
arrays that use them (decl-order); fixed-size locals capped by
ICE_NK_MAX (kappa-shear NZ_STACK_MAX precedent) in the driver.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| real(kind=wp), | public, | parameter | :: | ICE_BULK_SALINITY | = | 4.0_wp |
|
| real(kind=wp), | public, | parameter | :: | ICE_H_LO_LIM | = | 0.0_wp |
|
| real(kind=wp), | public, | parameter | :: | ICE_K_ICE | = | 2.03_wp |
Bulk ice thermal conductivity (W/m/K) — SIS2 |
| real(kind=wp), | public, | parameter | :: | ICE_K_SNOW | = | 0.31_wp |
Bulk snow thermal conductivity (W/m/K) — SIS2 |
| real(kind=wp), | public, | parameter | :: | ICE_RHO_ICE | = | 905.0_wp |
Nominal sea-ice density (kg/m³). |
| real(kind=wp), | public, | parameter | :: | ICE_RHO_OCEAN | = | 1030.0_wp |
Nominal seawater reference density (kg/m³) — SIS2 |
| real(kind=wp), | public, | parameter | :: | ICE_RHO_SNOW | = | 330.0_wp |
Nominal snow density (kg/m³). |
| real(kind=wp), | public, | parameter | :: | ICE_TEMP_RANGE_EST | = | 40.0_wp |
|
Per-layer implicit heat-budget solve for the new layer
temperature — SIS2 laytemp_SIS2 (SIS2_ice_thm.F90:544-700),
ICE_CP_BRINE == ICE_CP_ICE closed-form branches only (the
Newton/false-position refinement at :625-692 is dead code
under the simplification and is deliberately NOT ported).
Port of prototype sis2_column.py:21-55.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | m |
Layer mass (kg/m²). |
||
| real(kind=wp), | intent(in) | :: | t_fr |
Layer freezing temperature (degC); 0 for snow/fresh water. |
||
| real(kind=wp), | intent(in) | :: | qf |
Forcing heat flux into the layer (W/m²). |
||
| real(kind=wp), | intent(in) | :: | bf |
Implicit coupling coefficient to the neighbour temperature (W/m²/K). |
||
| real(kind=wp), | intent(in) | :: | tp |
Previous-step layer temperature (degC). |
||
| real(kind=wp), | intent(in) | :: | dtt |
Timestep (s). |
Per-(cell,category) orchestrator: gather (bottom-up -> top-down
flip, TRAP #2), optics, conduction (ice_temp_sis2), resize
(snow add, bottom-freeze, top/bottom melt peel, rebalance),
scatter (flip back).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nk |
Number of ice layers (declared first — decl-order). |
||
| real(kind=wp), | intent(inout) | :: | m_snow |
Snow mass per unit area (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | m_ice_tot |
Total ice mass per unit area (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | enth_snow_pt |
Snow specific enthalpy (J/kg). |
||
| 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(in) | :: | sf_0 |
Linearized SEB intercept (W/m²), upward-positive. |
||
| real(kind=wp), | intent(in) | :: | dsf_dt |
Linearized SEB slope (W/m²/K), upward-positive. |
||
| real(kind=wp), | intent(in) | :: | sw_dn |
Downwelling shortwave at the surface (W/m²). |
||
| real(kind=wp), | intent(in) | :: | tfw |
Seawater freezing temperature at the ice base (degC). |
||
| real(kind=wp), | intent(in) | :: | fb |
Ocean -> ice-base heat flux (W/m²). |
||
| 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 but kept for
call-site parity, |
||
| real(kind=wp), | intent(in) | :: | dtt |
Timestep (s). |
||
| logical, | intent(in) | :: | do_snow_ice |
Archimedes freeboard flood gate ( |
||
| real(kind=wp), | intent(in) | :: | snow |
New snow mass this window (kg/m²), |
||
| real(kind=wp), | intent(out) | :: | tsurf |
Surface skin temperature (degC). |
||
| real(kind=wp), | intent(out) | :: | h2o_ocn_to_ice |
Mass flux frozen from the ocean onto the ice base (kg/m²). |
||
| real(kind=wp), | intent(out) | :: | h2o_ice_to_ocn |
Meltwater mass flux to the ocean (kg/m²), top + bottom peel. |
||
| real(kind=wp), | intent(out) | :: | heat_to_ocn |
Leftover melt heat dumped to the ocean (J/m²), top + bottom. |
||
| real(kind=wp), | intent(out) | :: | sw_thru |
Shortwave transmitted through the ice to the ocean (W/m²). |
||
| real(kind=wp), | intent(out) | :: | snow_to_ice |
Mass converted from snow to the top ice layer this call
(kg/m²), >= 0 — SIS2 |
SEB + vertical-conduction column solve — SIS2 ice_temp_SIS2
(SIS2_ice_thm.F90:169-540). Port of prototype
sis2_column.py:151-412. TOP-DOWN column (index 0 = snow,
1..nk = ice top->bottom) — see module docstring TRAP #2.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nk |
Number of ice layers (declared first — decl-order). |
||
| real(kind=wp), | intent(in) | :: | m_snow |
Snow mass per unit area (kg/m²). |
||
| real(kind=wp), | intent(in) | :: | m_ice_tot |
Total ice mass per unit area (kg/m²). |
||
| real(kind=wp), | intent(in) | :: | sice(nk) |
TOP-DOWN ice bulk salinities (PSU). |
||
| real(kind=wp), | intent(inout) | :: | enthalpy(0:nk) |
TOP-DOWN specific enthalpies (J/kg): 0 = snow, 1..nk = ice. |
||
| real(kind=wp), | intent(in) | :: | sf_0 |
Linearized SEB intercept (W/m²), upward-positive: |
||
| real(kind=wp), | intent(in) | :: | dsf_dt |
Linearized SEB slope (W/m²/K), upward-positive. |
||
| real(kind=wp), | intent(in) | :: | sol(0:nk) |
Absorbed solar per layer (W/m²), TOP-DOWN. |
||
| real(kind=wp), | intent(in) | :: | tfw |
Seawater freezing temperature at the ice base (degC). |
||
| real(kind=wp), | intent(in) | :: | fb |
Ocean -> ice-base heat flux (W/m²); post-hoc residual only — TRAP #3. |
||
| real(kind=wp), | intent(in) | :: | dtt |
Timestep (s). |
||
| real(kind=wp), | intent(out) | :: | tsurf |
Surface skin temperature (degC). |
||
| real(kind=wp), | intent(inout) | :: | tmelt |
Accumulated top melting energy (J/m²); caller zeroes per step. |
||
| real(kind=wp), | intent(inout) | :: | bmelt |
Accumulated bottom melting/freezing energy (J/m²); caller zeroes per step. |
||
| real(kind=wp), | intent(out) | :: | col_enth_in |
Column enthalpy Σ m_lay*enth BEFORE anything (diag). |
||
| real(kind=wp), | intent(out) | :: | col_enth_out |
Column enthalpy Σ m_lay*enth AFTER the conservative update, BEFORE the liq-lim clamp (diag). |
||
| real(kind=wp), | intent(out) | :: | sum_sol |
Σ sol*dtt over the column (diag, J/m²). |
||
| real(kind=wp), | intent(out) | :: | tflux_sfc |
Time-integrated surface heat flux into the column (diag, J/m²). |
||
| real(kind=wp), | intent(out) | :: | tflux_bot |
Time-integrated basal heat flux into the column (diag, J/m²). |
do concurrent cell driver: PHYSICAL cells only, inner if
gate (never a masked DC header), serial do cat loop inside.
Per (i,j,cat): outputs zeroed unconditionally, then gated on
wet_mask > 0.5 .and. m_ice > ICE_RHO_ICE*H_VANISHED (dynamic-
vanish taxonomy: skip intact, never clamp/divide a vanished
column). part_size is deliberately NOT an argument — thermo
is per unit ice area.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | nghost |
Grid + category + layer extents (declared first — decl-order). |
||
| integer, | intent(in) | :: | nx |
Grid + category + layer extents (declared first — decl-order). |
||
| integer, | intent(in) | :: | ny |
Grid + category + layer extents (declared first — decl-order). |
||
| integer, | intent(in) | :: | ncat |
Grid + category + layer extents (declared first — decl-order). |
||
| integer, | intent(in) | :: | nk |
Grid + category + layer extents (declared first — decl-order). |
||
| real(kind=wp), | intent(in) | :: | dtt |
Timestep (s). |
||
| logical, | intent(in) | :: | do_snow_ice |
Archimedes freeboard flood gate ( |
||
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) |
Ocean wet mask (>0.5 = wet). |
||
| real(kind=wp), | intent(inout) | :: | m_ice(nx,ny,ncat) |
Total ice mass per unit area per category (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | m_snow(nx,ny,ncat) |
Snow mass per unit area per category (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | enth_ice(nx,ny,ncat,nk) |
Ice specific enthalpy (J/kg), BOTTOM-UP (k=1 = ice bottom). |
||
| real(kind=wp), | intent(inout) | :: | enth_snow(nx,ny,ncat,1) |
Snow specific enthalpy (J/kg). |
||
| real(kind=wp), | intent(inout) | :: | sal_ice(nx,ny,ncat,nk) |
Ice bulk salinity (PSU), BOTTOM-UP. |
||
| real(kind=wp), | intent(in) | :: | sf_0(nx,ny) |
Linearized SEB intercept (W/m²), upward-positive. |
||
| real(kind=wp), | intent(in) | :: | dsf_dt(nx,ny) |
Linearized SEB slope (W/m²/K), upward-positive. |
||
| real(kind=wp), | intent(in) | :: | sw_dn(nx,ny) |
Downwelling shortwave at the surface (W/m²). |
||
| real(kind=wp), | intent(in) | :: | fprec(nx,ny) |
Frozen-precipitation rate onto the ice top (kg/m²/s), >= 0 —
PR 26 snowfall seam ( |
||
| real(kind=wp), | intent(in) | :: | tfw(nx,ny) |
Seawater freezing temperature at the ice base (degC). |
||
| real(kind=wp), | intent(in) | :: | fb(nx,ny) |
Ocean -> ice-base heat flux (W/m²). |
||
| real(kind=wp), | intent(in) | :: | sst(nx,ny) |
Sea-surface temperature (degC). |
||
| real(kind=wp), | intent(in) | :: | s_surf(nx,ny) |
Sea-surface salinity (PSU). |
||
| real(kind=wp), | intent(inout) | :: | tsurf_out(nx,ny,ncat) |
Surface skin temperature (degC). |
||
| real(kind=wp), | intent(inout) | :: | h2o_ocn_to_ice(nx,ny,ncat) |
Mass flux frozen from the ocean onto the ice base (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | h2o_ice_to_ocn(nx,ny,ncat) |
Meltwater mass flux to the ocean (kg/m²). |
||
| real(kind=wp), | intent(inout) | :: | heat_to_ocn(nx,ny,ncat) |
Leftover melt heat dumped to the ocean (J/m²). |
||
| real(kind=wp), | intent(inout) | :: | sw_thru(nx,ny,ncat) |
Shortwave transmitted through the ice to the ocean (W/m²). |
||
| real(kind=wp), | intent(inout) | :: | snow_to_ice(nx,ny,ncat) |
Mass converted from snow to the top ice layer this call
(kg/m²), >= 0 — SIS2 |
Conservative per-layer implicit enthalpy update — SIS2
update_lay_enth (SIS2_ice_thm.F90:704-945), closed-form
branches only. Port of prototype sis2_column.py:58-135.
Four solution branches (massless layer; pin-to-max with
banked extra_enth; fresh sice==0 linear; salty quadratic),
then the three-way explicit-vs-conservation-inverted flux
bookkeeping (prototype :117-133, incl. the denom > 0 guard).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | m_lay |
Layer mass (kg/m²). |
||
| real(kind=wp), | intent(in) | :: | sice |
Layer bulk salinity (PSU); 0 for snow/fresh. |
||
| real(kind=wp), | intent(inout) | :: | enth |
Layer specific enthalpy (J/kg); in = prior step, out = new. |
||
| real(kind=wp), | intent(inout) | :: | ftop |
Heat flux at the layer’s top interface (W/m²); in = prior estimate, out = updated (explicit or conservation-inverted). |
||
| real(kind=wp), | intent(in) | :: | ht_body |
In-layer heating (solar absorption) (W/m²). |
||
| real(kind=wp), | intent(inout) | :: | fbot |
Heat flux at the layer’s bottom interface (W/m²); in/out as
|
||
| real(kind=wp), | intent(in) | :: | dftop_dt |
d(ftop)/d(new_temp) (W/m²/K). |
||
| real(kind=wp), | intent(in) | :: | dfbot_dt |
d(fbot)/d(new_temp) (W/m²/K). |
||
| real(kind=wp), | intent(in) | :: | dtt |
Timestep (s). |
||
| real(kind=wp), | intent(in) | :: | hf_err_rat |
Precomputed |
||
| real(kind=wp), | intent(out) | :: | extra_heat |
Banked excess heat when pinned to |
||
| real(kind=wp), | intent(out) | :: | new_temp |
Resulting layer temperature (degC). |
||
| logical, | intent(in) | :: | has_temp_max |
True when an explicit |
||
| real(kind=wp), | intent(in) | :: | temp_max |
Explicit temperature ceiling (degC), used only when
|