rdb_ice_mass.F90 Source File

Sea-ice column mass bookkeeping (SIS2 port, PR 3a).


This file depends on

sourcefile~~rdb_ice_mass.f90~~EfferentGraph sourcefile~rdb_ice_mass.f90 rdb_ice_mass.F90 sourcefile~rdb_constants.f90 rdb_constants.F90 sourcefile~rdb_ice_mass.f90->sourcefile~rdb_constants.f90 sourcefile~rdb_ice_enthalpy.f90 rdb_ice_enthalpy.F90 sourcefile~rdb_ice_mass.f90->sourcefile~rdb_ice_enthalpy.f90 sourcefile~rdb_ice_enthalpy.f90->sourcefile~rdb_constants.f90

Files dependent on this one

sourcefile~~rdb_ice_mass.f90~~AfferentGraph sourcefile~rdb_ice_mass.f90 rdb_ice_mass.F90 sourcefile~rdb_ice_column.f90 rdb_ice_column.F90 sourcefile~rdb_ice_column.f90->sourcefile~rdb_ice_mass.f90 sourcefile~rdb_ice_frazil_uptake.f90 rdb_ice_frazil_uptake.F90 sourcefile~rdb_ice_frazil_uptake.f90->sourcefile~rdb_ice_mass.f90 sourcefile~rdb_ice_frazil_uptake.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_state.f90 rdb_ice_state.F90 sourcefile~rdb_ice_frazil_uptake.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_basal_flux.f90 rdb_ice_basal_flux.F90 sourcefile~rdb_ice_basal_flux.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_basal_flux.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_evp.f90 rdb_ice_evp.F90 sourcefile~rdb_ice_evp.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_evp.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_init.f90 rdb_ice_init.F90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_init.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_state.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_thermo_driver.f90 rdb_ice_thermo_driver.F90 sourcefile~rdb_ice_thermo_driver.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_thermo_driver.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_transport.f90 rdb_ice_transport.F90 sourcefile~rdb_ice_transport.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ice_transport.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_itd.f90 rdb_ice_itd.F90 sourcefile~rdb_ice_transport.f90->sourcefile~rdb_ice_itd.f90 sourcefile~rdb_ocean_halo_state.f90 rdb_ocean_halo_state.F90 sourcefile~rdb_ice_transport.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_console_stats.f90 rdb_ocean_console_stats.F90 sourcefile~rdb_ocean_console_stats.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ocean_diag_fills.f90 rdb_ocean_diag_fills.F90 sourcefile~rdb_ocean_diag_fills.f90->sourcefile~rdb_ice_column.f90 sourcefile~rdb_ocean_state.f90 rdb_ocean_state.F90 sourcefile~rdb_ocean_diag_fills.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_ocean_engine.f90 rdb_ocean_engine.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_frazil_uptake.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_basal_flux.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_evp.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_init.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_thermo_driver.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_transport.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_diag_fills.f90 sourcefile~rdb_config.f90 rdb_config.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ice_atm_forcing.f90 rdb_ice_atm_forcing.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_atm_forcing.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_itd.f90 sourcefile~rdb_ice_ocean_coupler.f90 rdb_ice_ocean_coupler.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_ocean_coupler.f90 sourcefile~rdb_ice_snow.f90 rdb_ice_snow.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ice_snow.f90 sourcefile~rdb_ocean_diag_derived.f90 rdb_ocean_diag_derived.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_diag_derived.f90 sourcefile~rdb_ocean_dyn.f90 rdb_ocean_dyn.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_decomp.f90 rdb_decomp.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_data_forcing.f90 rdb_ocean_data_forcing.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_data_forcing.f90 sourcefile~rdb_ocean_data_input.f90 rdb_ocean_data_input.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_data_input.f90 sourcefile~rdb_ocean_setup.f90 rdb_ocean_setup.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_setup.f90 sourcefile~rdb_ocean_stability_audit.f90 rdb_ocean_stability_audit.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_ocean_stability_audit.f90 sourcefile~rdb_state.f90 rdb_state.F90 sourcefile~rdb_ocean_engine.f90->sourcefile~rdb_state.f90 sourcefile~rdb_config.f90->sourcefile~rdb_ice_init.f90 sourcefile~rdb_driver.f90 rdb_driver.F90 sourcefile~rdb_driver.f90->sourcefile~rdb_ocean_console_stats.f90 sourcefile~rdb_driver.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_driver.f90->sourcefile~rdb_config.f90 sourcefile~rdb_driver.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_driver.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_handle.f90 rdb_handle.F90 sourcefile~rdb_handle.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_handle.f90->sourcefile~rdb_config.f90 sourcefile~rdb_handle.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_ice_atm_forcing.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_itd.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_ocean_coupler.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ice_ocean_coupler.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ice_snow.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ocean_api.f90 rdb_ocean_api.F90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_ocean_diag_fills.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_ocean_engine.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_handle.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_ocean_diag_derived.f90 sourcefile~rdb_ocean_api.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_diag_derived.f90->sourcefile~rdb_ocean_diag_fills.f90 sourcefile~rdb_ocean_diag_derived.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_console_stats.f90 sourcefile~rdb_ocean_dyn.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_halo_state.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ice_state.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_data_forcing.f90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_data_input.f90 sourcefile~rdb_ocean_z_init.f90 rdb_ocean_z_init.F90 sourcefile~rdb_ocean_state.f90->sourcefile~rdb_ocean_z_init.f90 sourcefile~rdb_config_schema.f90 rdb_config_schema.F90 sourcefile~rdb_config_schema.f90->sourcefile~rdb_config.f90 sourcefile~rdb_decomp.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_data_forcing.f90->sourcefile~rdb_ocean_data_input.f90 sourcefile~rdb_ocean_data_input.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_dyn.f90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_halo_state.f90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_ocean_state.f90 sourcefile~rdb_ocean_setup.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_stability_audit.f90->sourcefile~rdb_config.f90 sourcefile~rdb_ocean_z_init.f90->sourcefile~rdb_config.f90 sourcefile~rdb_state.f90->sourcefile~rdb_config.f90 sourcefile~rdb_halo.f90 rdb_halo.F90 sourcefile~rdb_halo.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_fold_exchange.f90 rdb_ocean_fold_exchange.F90 sourcefile~rdb_ocean_fold_exchange.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_halo.f90 rdb_ocean_halo.F90 sourcefile~rdb_ocean_halo.f90->sourcefile~rdb_decomp.f90 sourcefile~rdb_ocean_restart_io.f90 rdb_ocean_restart_io.F90 sourcefile~rdb_ocean_restart_io.f90->sourcefile~rdb_decomp.f90

Source Code

!! Sea-ice column mass bookkeeping (SIS2 port, PR 3a).
module rdb_ice_mass
   !! Ports of the subset of `ice_resize_SIS2` (SIS2_ice_thm.F90:1010-1338)
   !! needed by the Winton column step: bottom freezing, top/bottom melt
   !! peel, and equal-mass layer rebalancing
   !! (`rebalance_ice_layers`, SIS2_ice_thm.F90:1448-1512). Ported from
   !! the validated prototype `sis2_resize.py` (Apache-2.0 source
   !! attribution as above).
   !!
   !! PR 26 ports the snow source term (`ice_snow_accumulate`, below) —
   !! the first branch SIS2 runs in `ice_resize_SIS2`
   !! (SIS2_ice_thm.F90:1122), so it sits first in this module too.
   !! PR 27 ports the Archimedes freeboard snow-ice flooding conversion
   !! (`ice_snow_ice_flood`, below) — the FINAL substantive block of
   !! `ice_resize_SIS2` (SIS2_ice_thm.F90:1303-1320), so it sits last in
   !! this module too. Pond/evap/rain mass paths remain deliberately NOT
   !! ported. TODO(PR-35): pond mass paths. Evap/rain need atmospheric
   !! fields Roundabout does not carry (PR-55 at the earliest).
   !!
   !! All procedures pure, `!$acc routine seq`, explicit-shape,
   !! TOP-DOWN columns (index 0 = snow, 1..nk = ice top->bottom) —
   !! identical convention to `rdb_ice_column`.
   use rdb_constants, only: wp
   use rdb_ice_enthalpy, only: ICE_LAT_FUS, ICE_LIQ_LIM, ICE_NK_MAX, &
                               ice_enthalpy_liquid_freeze
   implicit none
   private

   public :: ice_snow_accumulate
   public :: ice_bottom_freeze, ice_top_melt_peel, ice_bottom_melt_peel, &
             ice_rebalance_layers, ice_snow_ice_flood

contains

   pure subroutine ice_snow_accumulate(nk, m_lay, snow)
      !! Snow source-term branch of `ice_resize_SIS2`
      !! (SIS2_ice_thm.F90:1122): `m_lay(0) = m_lay(0) + snow`. The
      !! snow layer's specific enthalpy `enthalpy(0)` is UNCHANGED by
      !! snowfall -- SIS2 is explicit that this "should do nothing"
      !! (SIS_slow_thermo.F90:1032-1033) and books the implied energy
      !! against the atmosphere (`enth_snowfall = snow*enthalpy(0)` ->
      !! `Enth_Mass_in_atm`). Roundabout's atmosphere is a prescribed slab
      !! with no energy budget to charge, so that bookkeeping is not
      !! ported -- a documented v1 divergence (PLAN_PR26_snowfall.md, S3).
      !!
      !! The SIS2 guard `if (mtot_ice == 0.0) m_lay(0) = 0.0`
      !! (SIS2_ice_thm.F90:1121, needed because SIS2 gates its column on
      !! `part_size > 0` and so can reach a category with zero ice mass)
      !! is deliberately NOT ported: `ice_column_step`'s caller only
      !! ever reaches this routine on a category that already passed
      !! the column's own entry gate (`m_ice > ICE_RHO_ICE*H_VANISHED`,
      !! `rdb_ice_column.F90`), so `mtot_ice > 0` always holds here. The
      !! ice-free share of the snowfall is routed to the ocean by
      !! `rdb_ice_snow%ice_snowfall_ocean_share` instead of being added
      !! as orphan snow (which would trip `rdb_ice_transport`'s
      !! fail-loud `mca_snow > 0` where `mca_ice <= 0` reduction).
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first -- decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m^2), 0 = snow, 1..nk = ice.
      real(wp), intent(in) :: snow
         !! New snow mass this thermo window (kg/m^2), `fprec*dt_therm`.
         !! `snowfall=0` => `snow==0.0_wp` => the guard below makes this
         !! call bit-identical to a no-op by inspection, not IEEE luck.

      if (snow /= 0.0_wp) then
         m_lay(0) = m_lay(0) + snow
      end if
   end subroutine ice_snow_accumulate

   pure subroutine ice_bottom_freeze(nk, m_lay, enthalpy, salin, bmelt, &
                                     enth_ocean, salin_freeze, h2o_ocn_to_ice)
      !! Bottom-freezing branch of `ice_resize_SIS2`
      !! (SIS2_ice_thm.F90:1164-1188; prototype sis2_resize.py:20-45).
      !! When `bmelt < 0` (net upward heat deficit at the base), freeze
      !! ocean water onto the bottom ice layer: `enth_freeze =
      !! min(enthalpy(nk), enth_ocean - min_denth_freeze)`,
      !! `m_freeze = -bmelt/(enth_ocean - enth_freeze)`, mass-weighted
      !! mix of the bottom layer's enthalpy + salinity, `bmelt` reset
      !! to 0.
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first — decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m²), 0 = snow, 1..nk = ice.
      real(wp), intent(inout) :: enthalpy(0:nk)
         !! Layer specific enthalpies (J/kg).
      real(wp), intent(inout) :: salin(0:nk)
         !! Layer bulk salinities (PSU).
      real(wp), intent(inout) :: bmelt
         !! Accumulated bottom melting/freezing energy (J/m²); reset
         !! to 0 on exit when freezing occurred.
      real(wp), intent(in) :: enth_ocean
         !! Ocean-water specific enthalpy at the ice base (J/kg) —
         !! TRAP #1: the LIQUID formula `ice_enthalpy_liquid(sst,
         !! s_surf)`, never the frozen/mushy `ice_enth_from_ts`.
      real(wp), intent(in) :: salin_freeze
         !! Salinity of newly frozen ice (PSU) — bulk-salinity mode:
         !! `ICE_BULK_SALINITY`.
      real(wp), intent(out) :: h2o_ocn_to_ice
         !! Mass flux frozen from the ocean onto the ice base (kg/m²).

      real(wp) :: min_denth_freeze, enth_freeze, m_freeze

      h2o_ocn_to_ice = 0.0_wp
      if (bmelt < 0.0_wp) then
         min_denth_freeze = ICE_LAT_FUS*(1.0_wp - ICE_LIQ_LIM)
         enth_freeze = min(enthalpy(nk), enth_ocean - min_denth_freeze)
         m_freeze = -bmelt/(enth_ocean - enth_freeze)

         enthalpy(nk) = (m_lay(nk)*enthalpy(nk) + m_freeze*enth_freeze)/ &
                        (m_lay(nk) + m_freeze)
         salin(nk) = (m_lay(nk)*salin(nk) + m_freeze*salin_freeze)/ &
                     (m_lay(nk) + m_freeze)
         m_lay(nk) = m_lay(nk) + m_freeze
         h2o_ocn_to_ice = m_freeze
         bmelt = 0.0_wp
      end if
   end subroutine ice_bottom_freeze

   pure subroutine ice_top_melt_peel(nk, m_lay, enthalpy, salin, tmelt, &
                                     heat_to_ocn, h2o_ice_to_ocn)
      !! Top melt peel (SIS2_ice_thm.F90:1217-1242; prototype
      !! sis2_resize.py:48-80). Peels mass from k=0 (snow) upward
      !! through k=nk until `tmelt` is spent; a massless layer is
      !! skipped; the partial layer takes `m_melt = melt_left/(enth_fr
      !! - enthalpy)`; any leftover melt energy (all layers exhausted)
      !! drains to `heat_to_ocn`. Snow/pond-free path — `tmelt` is
      !! assumed already >= 0 on entry (the caller folds any negative
      !! top-melt into `bmelt` upstream, SIS2:1159-1163).
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first — decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m²), 0 = snow, 1..nk = ice.
      real(wp), intent(inout) :: enthalpy(0:nk)
         !! Layer specific enthalpies (J/kg).
      real(wp), intent(in) :: salin(0:nk)
         !! Layer bulk salinities (PSU) — index 0 (snow) unused.
      real(wp), intent(in) :: tmelt
         !! Accumulated top melting energy (J/m²), assumed >= 0.
      real(wp), intent(inout) :: heat_to_ocn
         !! Leftover melt energy after all layers exhausted (J/m²) —
         !! accumulator, caller zeroes once per step.
      real(wp), intent(inout) :: h2o_ice_to_ocn
         !! Meltwater mass flux to the ocean (kg/m²) — accumulator.

      real(wp) :: enth_fr(0:ICE_NK_MAX)
      real(wp) :: melt_left, avail, m_melt
      integer :: k

      enth_fr(0) = ice_enthalpy_liquid_freeze(0.0_wp)
      do k = 1, nk
         enth_fr(k) = ice_enthalpy_liquid_freeze(salin(k))
      end do

      melt_left = tmelt
      if (melt_left > 0.0_wp) then
         do k = 0, nk
            if (m_lay(k) <= 0.0_wp) cycle
            avail = m_lay(k)*(enth_fr(k) - enthalpy(k))
            if (melt_left < avail) then
               m_melt = melt_left/(enth_fr(k) - enthalpy(k))
               melt_left = 0.0_wp
            else
               m_melt = m_lay(k)
               melt_left = melt_left - avail
            end if
            m_lay(k) = m_lay(k) - m_melt
            h2o_ice_to_ocn = h2o_ice_to_ocn + m_melt
            if (melt_left <= 0.0_wp) exit
         end do
         heat_to_ocn = heat_to_ocn + melt_left
      end if
   end subroutine ice_top_melt_peel

   pure subroutine ice_bottom_melt_peel(nk, m_lay, enthalpy, salin, bmelt, &
                                        heat_to_ocn, h2o_ice_to_ocn)
      !! Bottom melt peel (SIS2_ice_thm.F90:1246-1271; prototype
      !! sis2_resize.py:83-111). Same peel as `ice_top_melt_peel` but
      !! from k=nk down to k=0. The prototype's separate `ablation`
      !! return is dropped (it is `h2o_ice_to_ocn` restricted to this
      !! call; PR 3b can re-derive it if needed).
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first — decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m²), 0 = snow, 1..nk = ice.
      real(wp), intent(inout) :: enthalpy(0:nk)
         !! Layer specific enthalpies (J/kg).
      real(wp), intent(in) :: salin(0:nk)
         !! Layer bulk salinities (PSU) — index 0 (snow) unused.
      real(wp), intent(inout) :: bmelt
         !! Accumulated bottom melting energy (J/m²), consumed by the
         !! peel; NOTE unlike `ice_bottom_freeze` this is intent
         !! inout only for interface symmetry — the peel does not
         !! reset it (caller passes the post-freeze residual).
      real(wp), intent(inout) :: heat_to_ocn
         !! Leftover melt energy after all layers exhausted (J/m²) —
         !! accumulator, caller zeroes once per step.
      real(wp), intent(inout) :: h2o_ice_to_ocn
         !! Meltwater mass flux to the ocean (kg/m²) — accumulator.

      real(wp) :: enth_fr(0:ICE_NK_MAX)
      real(wp) :: melt_left, avail, m_melt
      integer :: k

      enth_fr(0) = ice_enthalpy_liquid_freeze(0.0_wp)
      do k = 1, nk
         enth_fr(k) = ice_enthalpy_liquid_freeze(salin(k))
      end do

      melt_left = bmelt
      if (melt_left > 0.0_wp) then
         do k = nk, 0, -1
            if (m_lay(k) <= 0.0_wp) cycle
            avail = m_lay(k)*(enth_fr(k) - enthalpy(k))
            if (melt_left < avail) then
               m_melt = melt_left/(enth_fr(k) - enthalpy(k))
               melt_left = 0.0_wp
            else
               m_melt = m_lay(k)
               melt_left = melt_left - avail
            end if
            m_lay(k) = m_lay(k) - m_melt
            h2o_ice_to_ocn = h2o_ice_to_ocn + m_melt
            if (melt_left <= 0.0_wp) exit
         end do
         heat_to_ocn = heat_to_ocn + melt_left
      end if
   end subroutine ice_bottom_melt_peel

   pure subroutine ice_rebalance_layers(nk, m_lay, enthalpy, salin, mtot_ice)
      !! Equal-mass repartition of ice layers 1..nk, mass-weighting
      !! enthalpy and salinity (SIS2_ice_thm.F90:1448-1512; prototype
      !! sis2_resize.py:114-161). The snow slot (`m_lay(0)`,
      !! `enthalpy(0)`) is untouched. `mtot_ice = sum(m_lay(1:nk))` is
      !! returned; `mtot_ice == 0` is an early exit leaving the ice
      !! layers untouched (there is no ice to rebalance). The k1/k2
      !! two-pointer drain loop follows the SIS2/prototype branch
      !! order exactly, including the
      !! `(m_ice_avg - mlay_new(k2) > src_m(k1)) .or. (k2 == nk)` test.
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first — decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m²), 0 = snow (untouched), 1..nk = ice.
      real(wp), intent(inout) :: enthalpy(0:nk)
         !! Layer specific enthalpies (J/kg).
      real(wp), intent(inout) :: salin(0:nk)
         !! Layer bulk salinities (PSU).
      real(wp), intent(out) :: mtot_ice
         !! Summed ice mass (kg/m²), `sum(m_lay(1:nk))` on entry.

      real(wp) :: mlay_new(ICE_NK_MAX), enth_ice_new(ICE_NK_MAX), sal_ice_new(ICE_NK_MAX)
      real(wp) :: src_m(ICE_NK_MAX)
      real(wp) :: m_ice_avg, m_transfer
      integer :: k1, k2, k

      mtot_ice = 0.0_wp
      do k = 1, nk
         mtot_ice = mtot_ice + m_lay(k)
      end do
      if (mtot_ice == 0.0_wp) return

      do k = 1, nk
         mlay_new(k) = 0.0_wp
         enth_ice_new(k) = 0.0_wp
         sal_ice_new(k) = 0.0_wp
         src_m(k) = m_lay(k)
      end do
      m_ice_avg = mtot_ice/real(nk, wp)

      k1 = 1
      k2 = 1
      do
         if (mlay_new(k2) >= m_ice_avg .and. k2 < nk) then
            k2 = k2 + 1
         else if (src_m(k1) <= 0.0_wp) then
            k1 = k1 + 1
         else if ((m_ice_avg - mlay_new(k2) > src_m(k1)) .or. (k2 == nk)) then
            m_transfer = src_m(k1)
            enth_ice_new(k2) = enth_ice_new(k2) + m_transfer*enthalpy(k1)
            sal_ice_new(k2) = sal_ice_new(k2) + m_transfer*salin(k1)
            mlay_new(k2) = mlay_new(k2) + m_transfer
            src_m(k1) = 0.0_wp
            k1 = k1 + 1
         else
            m_transfer = m_ice_avg - mlay_new(k2)
            enth_ice_new(k2) = enth_ice_new(k2) + m_transfer*enthalpy(k1)
            sal_ice_new(k2) = sal_ice_new(k2) + m_transfer*salin(k1)
            mlay_new(k2) = m_ice_avg
            src_m(k1) = src_m(k1) - m_transfer
            k2 = k2 + 1
         end if
         if (k1 > nk) exit
      end do

      do k = 1, nk
         if (mlay_new(k) > 0.0_wp) then
            enthalpy(k) = enth_ice_new(k)/mlay_new(k)
            salin(k) = sal_ice_new(k)/mlay_new(k)
         end if
         m_lay(k) = mlay_new(k)
      end do
   end subroutine ice_rebalance_layers

   pure subroutine ice_snow_ice_flood(nk, m_lay, enthalpy, salin, rho_ratio, snow_to_ice)
      !! Archimedes freeboard snow-ice flooding — the FINAL substantive
      !! block of `ice_resize_SIS2` (SIS2_ice_thm.F90:1303-1320; PR 27).
      !! Standard closure: Leppäranta (1983), *A growth model for black
      !! ice, snow ice and snow thickness in subarctic basins*, Nordic
      !! Hydrology 14, 59-70; Fichefet & Morales Maqueda (1997), JGR 102,
      !! 12609-12646 §2.3.
      !!
      !! The column floats, displacing its own mass of seawater. If the
      !! ice alone cannot support the snow load (`m_submerged =
      !! (m_i+m_s)*rho_ratio > m_i`, `rho_ratio = ICE_RHO_ICE/
      !! ICE_RHO_OCEAN`), the snow-ice interface is below the waterline
      !! ("flooded"): convert `snow_to_ice = min(m_submerged - m_i,
      !! m_lay(0))` kg/m^2 of snow into the TOP ice layer (local index 1
      !! — TOP-DOWN column, TRAP #2 in `rdb_ice_column`'s module
      !! docstring; the caller flips this to the state's bottom-up
      !! `enth_ice(...,nk)` at the scatter boundary). One non-iterative
      !! step suffices: the conversion is 1:1 in mass, so `m_i+m_lay(0)`
      !! is invariant and `m_submerged` does not move, so the interface
      !! lands EXACTLY on the waterline (see PLAN_PR27_snow_ice_
      !! flooding.md §3 for the algebraic proof + a worked golden).
      !!
      !! Roundabout has no ponds (`m_pond` is a hardwired dead local in
      !! `rdb_ice_column`), so the `min(...)` clamp is PROVABLY DEAD
      !! CODE here: it binds iff `m_lay(0) < -m_i`, impossible for
      !! non-negative masses (see the plan §9.2 sweep test). Kept for
      !! SIS2 parity — ponds would resurrect it.
      !!
      !! Mass-, enthalpy- and salt-conserving WITHIN THE COLUMN, exactly
      !! (`(m_0-s)*E_0 + (m_1+s)*[(m_1*E_1+s*E_0)/(m_1+s)] = m_0*E_0 +
      !! m_1*E_1`, identity; the salinity dilution `S_1*m_1/(m_1+s)` IS
      !! the mass-weighted mix since `S_snow == 0`, `salin(0)`).
      !! `enthalpy(1)` and `salin(1)` both DECREASE — snow's cold, fresh
      !! mass lands in the ice.
      !!
      !! **Known physical incompleteness, inherited from SIS2 (not fixed
      !! here — true seawater flooding is a materially larger, separate
      !! closure, see the plan's §12/§14 Q1).** Real flooding draws
      !! SEAWATER into the pore space, which then refreezes, releasing
      !! latent heat and rejecting brine. SIS2 instead moves snow mass
      !! with the SNOW's enthalpy and ZERO salinity — snow enthalpy
      !! (<= -L_f ~ -3.34e5 J/kg) is far more negative than near-freezing
      !! seawater, so the new ice is too COLD; `S_snow == 0` means it is
      !! too FRESH. `snow_to_ice` is diagnostic-only (SIS2's SN2IC):
      !! flooding exchanges NOTHING with the ocean (SIS2_ice_thm.F90:1273,
      !! "There are no further heat or mass losses or gains by the
      !! ice+snow").
      !$acc routine seq
      integer, intent(in) :: nk
         !! Number of ice layers (declared first — decl-order).
      real(wp), intent(inout) :: m_lay(0:nk)
         !! Layer masses (kg/m²), 0 = snow, 1..nk = ice.
      real(wp), intent(inout) :: enthalpy(0:nk)
         !! Layer specific enthalpies (J/kg).
      real(wp), intent(inout) :: salin(0:nk)
         !! Layer bulk salinities (PSU).
      real(wp), intent(in) :: rho_ratio
         !! `ICE_RHO_ICE/ICE_RHO_OCEAN` (nondim, < 1) — computed by the
         !! caller (`rdb_ice_column`), which already has both density
         !! parameters in scope; keeps this module free of a circular
         !! `use rdb_ice_column` (that module `use`s this one).
      real(wp), intent(out) :: snow_to_ice
         !! Mass converted from snow to the top ice layer this call
         !! (kg/m²), >= 0 — SIS2 `SN2IC`.

      real(wp) :: m_i, m_submerged

      m_i = sum(m_lay(1:nk))
      m_submerged = (m_i + m_lay(0))*rho_ratio

      snow_to_ice = 0.0_wp
      if (m_submerged > m_i) then
         snow_to_ice = min(m_submerged - m_i, m_lay(0))
      end if

      ! Denominator `m_lay(1) + snow_to_ice` is provably > 0 whenever
      ! this branch is taken: entering it forces `m_lay(0) > 0` (since
      ! rho_ratio < 1, m_submerged > m_i needs m_lay(0) > 0), hence
      ! snow_to_ice = min(m_lay(0), ...) > 0 — even if m_lay(1) == 0
      ! (top layer melted out this step). The inner guard below is free
      ! and makes the no-snow no-op provable by inspection; it is NOT
      ! `H_DIV_EPS` armour (CLAUDE.md taxonomy — the branch guard above
      ! already rules out the zero case).
      if (snow_to_ice > 0.0_wp) then
         m_lay(0) = m_lay(0) - snow_to_ice
         enthalpy(1) = (m_lay(1)*enthalpy(1) + snow_to_ice*enthalpy(0))/ &
                       (m_lay(1) + snow_to_ice)
         salin(1) = salin(1)*m_lay(1)/(m_lay(1) + snow_to_ice)
         m_lay(1) = m_lay(1) + snow_to_ice
      end if
   end subroutine ice_snow_ice_flood

end module rdb_ice_mass