Bridges ice_thermo_columns (PR 3a) into the ocean-coupling seam:
runs the column, then reduces h2o_ocn_to_ice/h2o_ice_to_ocn/heat_to_ocn
over categories into the per-cell heat_flux_diag/m_melt_diag and the
net-melt salt contribution added into salt_flux_diag.
Q_heat = sum_cat(heat_to_ocn)/dt_therm - fb [W/m^2, +down]
m_net = sum_cat(h2o_ocn_to_ice - h2o_ice_to_ocn) [kg/m^2, +freeze]
salt += m_net*(s_surf - ICE_BULK_SALINITY)/dt_therm (ADD, not set)
sw_thru_diag = sum_cat(sw_thru) [W/m^2, +down]
PR 31: sw_thru (the shortwave penetrating the ice to the water) is
now reduced to the per-cell sw_thru_diag and coupled to the ocean
by ice_ocean_sw_flux (rdb_ice_ocean_coupler). It is DELIBERATELY
kept OUT of the heat_flux_diag sum above — heat_flux_diag carries
only the NON-shortwave heat; the coupler delivers the shortwave via a
separate path (the q_sw component, or a direct Q_heat add) so the
energy reaches Q_heat exactly once in either component mode.
Ordering contract (driver-enforced, see rdb_driver.F90): this
module must run AFTER ice_frazil_uptake in the thermo-cadence
block. ice_frazil_uptake_impl OVERWRITES salt_flux_diag at loop
top (unconditional zero, then a gated write) — this module’s
reduction kernel ADDS its net-melt term on top of that write, never
zeroing salt_flux_diag itself. heat_flux_diag/m_melt_diag ARE
zeroed unconditionally here (this module owns them outright).
fb double-count guard: the column already folds fb into
bmelt (rdb_ice_column TRAP #3) and reports heat_to_ocn/
h2o_ice_to_ocn net of that. The only place fb re-enters the
energy accounting is the Q_heat formula above — never add it
anywhere else.
Reads the sst/s_surf/tfw sample seam filled by
rdb_ice_basal_flux%ice_compute_basal_flux earlier in the same
thermo-cadence block (one sample, reused — see that module’s
docstring).
PR 4a multicat dispatch: ncat == 1 runs the EXISTING
ice_thermo_driver_reduce_impl byte-for-byte UNCHANGED (bit-identity
contract). ncat > 1 additionally fills ice%fb_part_sum — the
fb-charged ice-cover-fraction snapshot — in a small DC kernel BEFORE
ice_thermo_columns runs (using the SAME entry gate the column
itself uses, m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED, evaluated
PRE-column so a category that melts out entirely this step is still
counted in the weight it was actually charged fb under — counting
POST-column would leak part*fb*dt_therm of energy from the books
for exactly that category), then dispatches to
ice_thermo_driver_reduce_multicat_impl, which part-weights the
per-category sums and subtracts fb_part_sum*fb (rather than the
bare fb the ncat==1 path subtracts) from the heat diag.
PR 26 snowfall: when ice%has_snowfall, ice_snow_part_ocn_
fill_impl runs (same PRE-column-snapshot contract and reasoning as
ice_fb_part_sum_fill_impl — a category that melts out this window
must still be counted in the cover it actually caught snow under)
for BOTH ncat==1 and ncat>1 (unlike fb_part_sum, which is
ncat>1-only: at ncat==1 the ice-free share is a binary 0/1, not
a part-weighted sum, so there is no “skip at ncat==1” case here).
ice%atm_fprec is threaded into ice_thermo_columns unconditionally
(harmless zeros when has_snowfall is false). The two reduce impls
above are NOT touched by PR 26 — the ocean-bound share is delivered
separately by rdb_ice_snow%ice_snowfall_ocean_share, called by the
driver AFTER this step (mandated order, rdb_driver.F90).
PR 27 snow-ice flooding: ice%snow_ice is threaded into
ice_thermo_columns as do_snow_ice unconditionally (harmless
.false. when the knob is off); the flood is entirely
column-internal (SIS2’s ice_resize_SIS2, no ocean mass/heat/salt
exchange — see rdb_ice_mass%ice_snow_ice_flood’s docstring) and
therefore does NOT enter the Q_heat/m_net/salt reduction
above; ice%snow_to_ice is filled but not yet reduced/coupled
(same “filled, not consumed” contract as sw_thru).
Outer shim (outer-shim + flat-impl pattern): ms/eos are
accepted for call-site parity with the other thermo-cadence
kernels (ice_compute_basal_flux, ice_frazil_uptake) even
though this step reads its ocean-surface sample from the
ice%sst_seam/ssurf_seam/tfw_seam scratch (filled by
ice_compute_basal_flux earlier in the same window) rather
than re-deriving it from ms/eos directly — eos is unused
here but kept in the signature so a future revision that DOES
need a fresh EOS evaluation does not have to change every call
site.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(eos_t), | intent(in) | :: | eos | |||
| type(multilayer_state_t), | intent(in) | :: | ms | |||
| type(ocean_sea_ice_t), | intent(inout) | :: | ice | |||
| real(kind=wp), | intent(in) | :: | dt_therm |
Effective thermo timestep (s) — |
Fill fb_part_sum(i,j) = Σ_c part_size(i,j,c) restricted to
categories passing the column’s OWN entry gate
(m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED) — MUST run BEFORE
ice_thermo_columns mutates m_ice (module docstring). Device
kernel over PHYSICAL cells, inner if/serial do cat — never a
masked do concurrent header.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | part_size(nx,ny,0:ncat) | |||
| real(kind=wp), | intent(in) | :: | m_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | fb_part_sum(nx,ny) | |||
| integer, | intent(in) | :: | nghost | |||
| integer, | intent(in) | :: | ncat | |||
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny |
PR 26: fill snow_part_ocn(i,j) — the ice-FREE share of the cell
that a uniform snowfall lands on with no ice underneath — under
the SAME PRE-column-snapshot contract as ice_fb_part_sum_fill_
impl (module docstring, this module docstring’s PR-26
paragraph): MUST run BEFORE ice_thermo_columns mutates m_ice,
using the column’s OWN entry gate
(m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | part_size(nx,ny,0:ncat) | |||
| real(kind=wp), | intent(in) | :: | m_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(inout) | :: | snow_part_ocn(nx,ny) | |||
| integer, | intent(in) | :: | nghost | |||
| integer, | intent(in) | :: | ncat | |||
| integer, | intent(in) | :: | nx | |||
| integer, | intent(in) | :: | ny |
Device kernel over PHYSICAL cells (ghosts excluded). Reduces the
per-category column outputs into the per-cell coupling diags.
heat_flux_diag/sw_thru_diag/m_melt_diag are zeroed
unconditionally at loop top (this module owns them outright);
salt_flux_diag is NOT zeroed — ice_frazil_uptake (which runs
first this window, see the module docstring) already zeroed +
wrote it, and this kernel ADDS its net-melt contribution on top,
gated on wet to avoid land noise (an unwetted cell adds exactly 0).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | h2o_ocn_to_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | h2o_ice_to_ocn(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | heat_to_ocn(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | sw_thru(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | ssurf_seam(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | fb(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | heat_flux_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | sw_thru_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | m_melt_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) | :: | nx | |||
| integer, | intent(in) | :: | ny |
ncat>1 SIS2 ITD-mode reduce (module docstring). Same zero/gate/
ordering contract as ice_thermo_driver_reduce_impl — the only
change is PART-WEIGHTING the per-category sums (the column’s
h2o_*/heat_to_ocn/sw_thru outputs are per unit ICE-COVERED
area in this mode, so a per-cell total needs the part_size
weight) and subtracting fb_part_sum*fb instead of the bare fb
the ncat==1 path subtracts (fb is a per-cell flux; fb_part_sum
is the fraction of the cell it was actually charged against —
see the module docstring and rdb_ice_state%fb_part_sum).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | wet_mask(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | part_size(nx,ny,0:ncat) | |||
| real(kind=wp), | intent(in) | :: | h2o_ocn_to_ice(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | h2o_ice_to_ocn(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | heat_to_ocn(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | sw_thru(nx,ny,ncat) | |||
| real(kind=wp), | intent(in) | :: | ssurf_seam(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | fb(nx,ny) | |||
| real(kind=wp), | intent(in) | :: | fb_part_sum(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | heat_flux_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | sw_thru_diag(nx,ny) | |||
| real(kind=wp), | intent(inout) | :: | m_melt_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) | :: | nx | |||
| integer, | intent(in) | :: | ny |