ocean_sea_ice_t Derived Type

type, public :: ocean_sea_ice_t

Sea-ice state slot. All fields default to the inert (enable=.false.) configuration so an ocean run that never sets &ocean_ice_nml is byte-identical to a build without this slot.


Inherits

type~~ocean_sea_ice_t~~InheritsGraph type~ocean_sea_ice_t ocean_sea_ice_t type~evp_workspace_t evp_workspace_t type~ocean_sea_ice_t->type~evp_workspace_t evp_ws

Inherited by

type~~ocean_sea_ice_t~~InheritedByGraph type~ocean_sea_ice_t ocean_sea_ice_t type~ocean_state_t ocean_state_t type~ocean_state_t->type~ocean_sea_ice_t ice type~ocean_engine_t ocean_engine_t type~ocean_engine_t->type~ocean_state_t state type~ocean_handle_t ocean_handle_t type~ocean_handle_t->type~ocean_state_t state type~ocean_handle_t->type~ocean_engine_t engine

Components

Type Visibility Attributes Name Initial
real(kind=wp), public, allocatable :: atm_dsfdt(:,:)

SEB slope dSF/dT (W/m^2/K) — dsf_dt. Same seam contract.

real(kind=wp), public, allocatable :: atm_fprec(:,:)

PR 26: frozen-precipitation rate onto the ice top (kg/m^2/s),

= 0 — the seam ice_atm_forcing_restoring fills from &ocean_ice_nml snowfall exactly like atm_sw_dn <- sw_down. Same SCRATCH contract (refilled every thermo step, NOT restart- carried). PR 55 fills this from data instead of a uniform scalar; the shape/units/lifecycle are fixed by this PR (see rdb_ice_snow’s module docstring for the binding seam spec).

real(kind=wp), public, allocatable :: atm_sf0(:,:)

Net upward surface flux at T_surf=0 (W/m^2), the SEB intercept sf_0 fed to ice_thermo_columns. Filled every thermo step by ice_atm_forcing_restoring (v1) or the future bulk-flux subsystem. SCRATCH (recomputed each step) — NOT restart-carried.

real(kind=wp), public, allocatable :: atm_sw_dn(:,:)

Downwelling shortwave into the ice top (W/m^2) — sw_dn. Same.

logical, public :: dynamics = .false.

EVP rheology master switch (&ocean_ice_nml dynamics), set from config BEFORE init (same convention as enable/ transport). Gates the driver’s ice_evp_step call and the transport sampler skip; does NOT gate any allocation here — str_d/str_t/str_s/tau_a_*/fxoc/fyoc/tau_ocn_* are unconditional (cheap 2-D arrays), same contract as u_ice/v_ice.

logical, public :: enable = .false.

Master switch (&ocean_ice_nml enable). Off (default) ⇒ the parent state never inits / maps / steps this slot ⇒ no-op.

real(kind=wp), public, allocatable :: enth_ice(:,:,:,:)

Ice specific enthalpy (J/kg), shape (nx_total, ny_total, ncat, nk_ice). BOTTOM-UP: k=1 = ice BOTTOM (ocean side), k=nk_ice = ice TOP (atm/snow side) — the opposite of the SIS2/rdb_ice_column internal top-down convention; the flip happens at the ice_column_step gather/scatter boundary (rdb_ice_column’s module docstring, TRAP #2).

real(kind=wp), public, allocatable :: enth_snow(:,:,:,:)

Snow specific enthalpy (J/kg), shape (nx_total, ny_total, ncat, 1).

type(evp_workspace_t), public :: evp_ws

Persistent EVP-dynamics scratch (rdb_ice_evp’s subcycle kernels). Allocated + GPU-mapped ONLY when dynamics is on (see evp_workspace_t) — the successor to the retired rdb_ice_evp module-level save workspace. Rides ocean_state_enter_data via this slot’s enter_data.

real(kind=wp), public, allocatable :: fb(:,:)

Ocean -> ice-base heat flux (W/m^2), filled by rdb_ice_basal_flux%ice_compute_basal_flux from the above-freezing SST. SCRATCH (recomputed each step) — NOT restart-carried. Same lifecycle as the atm_* seam.

real(kind=wp), public, allocatable :: fb_part_sum(:,:)

Pre-column fb-charged ice-cover fraction snapshot (shape (nx_total, ny_total)) — Σ_c part_size(c) over the categories that pass the column’s OWN entry gate (m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED), evaluated POST-uptake / PRE-column by rdb_ice_thermo_driver so the weight matches exactly the categories the column charges fb to before ice_thermo_columns mutates m_ice. SCRATCH (recomputed every thermo step at ncat>1 only) — zero-init, NOT restart-carried, same lifecycle as the atm_*/fb seam fields.

real(kind=wp), public, allocatable :: fprec_ocn_diag(:,:)

PR 26: frozen precipitation delivered directly to the ocean (kg/m^2/s, per unit CELL area, >= 0) — the share of atm_fprec that lands where there is no ice (snow_part_ocn-weighted; open water at ncat>1, ice-free cells at ncat==1). Zero on land and on fully ice-covered cells. SCRATCH: zeroed + rewritten every thermo window by ice_snowfall_ocean_share, NOT restart-carried. Binding seam spec (consumed by PR-16 as a net_massin source): see rdb_ice_snow’s module docstring.

real(kind=wp), public, allocatable :: frazil_heat(:,:)

Ocean-side frazil supercooling BANK (J/m², T-cells, shape (nx_total, ny_total)). ice_frazil_accumulate clamps the surface layer at the freezing point and deposits the removed heat deficit ρ·Cp·h·(T_f − T)⁺ here; the ice thermodynamics (PR 3) will SPEND it as new-ice formation. Persistent state (accumulates across steps, restart-carried) — distinct from the drain-and-zero budget accumulator below.

real(kind=wp), public, allocatable :: fxoc(:,:)

Subcycle-averaged ice->ocean stress [Pa], u-faces, shape (nx_total+1, ny_total). SIS2 fxoc. Persistent, restart- carried (optional): ice_ocean_stress_flux blends this into ocean_surface_stress_t%tau_x every outer step. PR 63: this registration is no longer what makes the resume bit-exact — tau_ocn_x/tau_ocn_y below carry the blend’s OUTPUT directly, so the resume path never needs to reconstruct it from fxoc + the checkpointed ci. Kept restart-carried for diagnostic continuity across a resume (like m_frozen_diag/m_melt_diag) — its only cross-step reader inside a call is ice_evp_dynamics’s own zeroing at entry, so it carries no state INTO the EVP solve either.

real(kind=wp), public, allocatable :: fyoc(:,:)

Ditto, v-faces, shape (nx_total, ny_total+1). SIS2 fyoc.

real(kind=wp), public, allocatable :: h2o_ice_to_ocn(:,:,:)

Meltwater mass to the ocean (kg/m²), shape (nx_total, ny_total, ncat), >= 0. SCRATCH.

real(kind=wp), public, allocatable :: h2o_ocn_to_ice(:,:,:)

Mass FROZEN from the ocean onto the ice base (kg/m²), shape (nx_total, ny_total, ncat), >= 0. SCRATCH.

real(kind=wp), public, allocatable :: h_lim(:)

Category lower thickness limits (m), shape (1:ncat+1) — SIS2 cat_thick_lim (SIS_state_initialization.F90:45-78). h_lim(c) is the lower bound of category c; h_lim(ncat+1) is stored but never used as an upper cap (category ncat is unbounded above — SIS2 keeps it too). Computed at init by ice_itd_category_bounds from ncat and (PR-58) the optional hlim_cfg override below — NOT restart-carried (cheap to recompute, and doing so avoids a stale-ncat mismatch after a restart that changes ncat).

logical, public :: has_snowfall = .false.

Host-side dispatch gate for the PR 26 snowfall source term, latched from (cfg%ocean%ice%snowfall /= 0.0_wp) BEFORE init (same convention as transport/dynamics). Gates the pre-column snow_part_ocn fill and the ice_snowfall_ocean_ share contributor call; does NOT gate allocation of atm_fprec/ snow_part_ocn/fprec_ocn_diag (cheap 2-D arrays, same contract as atm_sw_dn/fb_part_sum) — the has_sw/has_heat idiom that makes snowfall=0 byte-identical by construction, not by IEEE argument.

real(kind=wp), public, allocatable :: heat_budget_frazil(:,:,:)

hTr (K·m) change per cell per outer step attributed to the frazil clamp — the budgets HEAT contributor mirroring the heat_budget_surface convention (positive = heat added to the ocean; the clamp WARMS the surface layer up to T_f). Shape (nx_total, ny_total, 1): the clamp only ever touches the surface layer, and the drain integrates over k anyway. Drained-and-zeroed by ocean_budgets_drain_contributors.

real(kind=wp), public, allocatable :: heat_flux_diag(:,:)

Ice -> ocean net surface heat flux from the last thermo window (W/m^2, POSITIVE DOWN into the ocean — the Q_heat convention), shape (nx_total, ny_total). Q_heat = heat_to_ocn/dt_therm - fb (see rdb_ice_thermo_driver). PERSISTENT + restart-carried (mirrors salt_flux_diag): rdb_ice_ocean_coupler refills Q_heat from this each thermo step, and the driver resume fold re-applies it. This is the NON-shortwave heat share only — the penetrating shortwave sw_thru_diag below is a SEPARATE field (PR 31), never folded in here (that would double-count against the q_sw component / the components-off Q_heat fold — see ice_ocean_sw_flux in rdb_ice_ocean_coupler).

real(kind=wp), public, allocatable :: heat_to_ocn(:,:,:)

Leftover melt energy drained back to the ocean (J/m²), shape (nx_total, ny_total, ncat), >= 0. SCRATCH.

real(kind=wp), public, allocatable :: hl_x_work(:,:)

PPM face-edge reconstruction of the category-SUMMED zonal mass, FACE-shaped (nx_total+1, ny_total) (mirrors rdb_continuity’s h_face_left_x). hl_x_work(i,j) is the value AT east face i extrapolated from the LEFT cell (i-1) (i.e. cell i-1’s own right/downwind edge); hr_x_work(i,j) is AT face i from the RIGHT cell i (cell i’s own left edge). FACE shape is load-bearing: the array-edge fallback writes index nx+1 (out of bounds for a (nx,ny) array — the pre-fix bug). SCRATCH, reused per medium.

real(kind=wp), public, allocatable :: hl_y_work(:,:)

North-face PPM edge, (nx_total, ny_total+1) — the meridional twin of hl_x_work (h_face_left_y convention). SCRATCH.

real(kind=wp), public, allocatable :: hlim_cfg(:)

hlimlist, trimmed to its supplied length, set byocean_state_init_from_configBEFOREinit. Unallocated => the SIS2 default table. NOT device-mapped (no kernel reads it —h_lim/mh_limare the mapped artefacts, computed atinitfrom this beforeenter_data) and NOT restart-carried (same lifecycle ash_lim`).

real(kind=wp), public, allocatable :: hr_x_work(:,:)

Right-cell zonal face-edge companion to hl_x_work, (nx_total+1, ny_total). SCRATCH.

real(kind=wp), public, allocatable :: hr_y_work(:,:)

North-face right-cell PPM edge, (nx_total, ny_total+1). SCRATCH.

real(kind=wp), public, allocatable :: htot_work(:,:)

Category-SUMMED mass, 2-D scratch (kg/m² of CELL area), shape (nx_total, ny_total). Reused for the ice solve then the snow solve, per direction (SIS2 zonal_mass_flux’s htot).

logical, public :: is_init = .false.

True between init and destroy; gate on this (never on allocated, which misses the GPU mapping).

real(kind=wp), public, allocatable :: m_frozen_diag(:,:)

New frazil-ice mass formed in the LAST thermo window (kg per m² of CELL area — v1 category convention, see rdb_ice_frazil_uptake), shape (nx_total, ny_total). OVERWRITTEN (not accumulated) each uptake; zero where no freezing occurred. Restart-carried for post-resume diagnostic continuity.

real(kind=wp), public, allocatable :: m_ice(:,:,:)

Total ice mass per category (kg/m²), shape (nx_total, ny_total, ncat). TWO-MODE CONVENTION: ncat==1 — per unit CELL area (legacy). ncat>1 — per unit ICE-COVERED area (SIS2 mH_ice, intensive quantity DIVIDED by part_size, not multiplied). Layer masses are always m_ice/nk_ice in EITHER mode — the column step always ends with ice_rebalance_layers, so equal-mass layering is a state invariant regardless of the area convention.

real(kind=wp), public, allocatable :: m_melt_diag(:,:)

Net meltwater mass to the ocean in the last window (kg/m^2 of cell, = h2o_ice_to_ocn - h2o_ocn_to_ice summed over cats; POSITIVE = net melt). OVERWRITTEN each window. Restart-carried for post-resume diagnostic continuity (like m_frozen_diag).

real(kind=wp), public, allocatable :: m_snow(:,:,:)

Snow mass per category (kg/m²), shape (nx_total, ny_total, ncat). Same two-mode area convention as m_ice (per-CELL at ncat==1, per-ICE-area at ncat>1).

real(kind=wp), public, allocatable :: mca_ice(:,:,:)

Cell-averaged ice mass per category (CAS space, kg/m² of CELL area — SIS2 CAS%m_ice), shape (nx_total, ny_total, ncat). SCRATCH: filled at Phase 1 (IST->CAS), consumed/updated through Phase 2, discarded after Phase 3 (CAS->IST).

real(kind=wp), public, allocatable :: mca_snow(:,:,:)

Cell-averaged snow mass per category (CAS space, kg/m² of CELL area), shape (nx_total, ny_total, ncat). Same lifecycle as mca_ice.

real(kind=wp), public, allocatable :: mh_lim(:)

Category lower MASS limits (kg/m²), shape (1:ncat+1), = ICE_RHO_ICE*h_lim — SIS2 mH_cat_bound (SIS_state_initialization.F90:75-77). ice_adjust_categories compares m_ice against THIS array (mass space, like SIS2), never against h_lim directly. Same non-restart-carried lifecycle as h_lim.

integer, public :: ncat = 5

Number of ice thickness categories (&ocean_ice_nml ncat). Category 0 is open water; SIS2 default 5. Sizes the per-category arrays when PR 3+ adds them.

integer, public :: nk_ice = 2

Vertical ice layers per category (&ocean_ice_nml nk_ice). 2 = Winton two-layer (v1). Bottom-up once arrays land (k=1 ice bottom — SIS2’s top-down index is flipped at this state layer, per the plan).

integer, public :: nx_total = 0
integer, public :: ny_total = 0
real(kind=wp), public, allocatable :: part_size(:,:,:)

Fractional ice-category area, shape (nx_total, ny_total, 0:ncat). Category 0 = OPEN WATER; Σ_cat part_size = 1. TWO-MODE CONVENTION (module docstring): ncat==1 — legacy lumped mode, deliberately UNTOUCHED by every kernel (part_size(:,:,0)=1 forever, category 1 unused, thermo runs per unit CELL area). ncat>1 — SIS2 ITD mode (PR 4a), LIVE: ice_frazil_uptake_multicat_impl annexes open water into an occupied category, and ice_adjust_categories restores the per-cell partition each thermo window.

real(kind=wp), public, allocatable :: sal_ice(:,:,:,:)

Ice bulk salinity (PSU), shape (nx_total, ny_total, ncat, nk_ice), same BOTTOM-UP k convention as enth_ice.

real(kind=wp), public, allocatable :: salt_flux_diag(:,:)

Ice -> ocean virtual salt flux from the last uptake (PSU·kg/m²/s, POSITIVE SALINIFIES — the surface-flux Q_salt convention), shape (nx_total, ny_total). PERSISTENT + restart-carried: rdb_ice_ocean_coupler refills Q_salt from this field each thermo step, and the driver’s configure-time resume fold re-applies it so the first post-resume window sees the same flux the uninterrupted run would have. PR 3c: the column driver ADDS its net-melt contribution here on top of what the frazil uptake (which runs first each window) wrote — see rdb_ice_thermo_driver.

logical, public :: snow_ice = .false.

Archimedes freeboard snow-ice flood master switch (&ocean_ice_nml snow_ice, PR 27), latched from config BEFORE init (same convention as transport/dynamics). Read by ice_thermo_driver_step and passed to ice_thermo_columns as do_snow_ice; does NOT gate allocation of snow_to_ice (cheap per-category array, same contract as sw_thru) — .false. (default) is what makes snow_ice=.false. byte-identical by construction.

real(kind=wp), public, allocatable :: snow_part_ocn(:,:)

PR 26: pre-column ice-free-cover fraction snapshot (shape (nx_total, ny_total)), 1 - Σ_c part_size(c) over the categories that pass the column’s OWN entry gate (m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED), evaluated PRE-column by ice_snow_part_ocn_fill_impl for the same reason as fb_part_sum — the weight must match the cover snow actually caught, before ice_thermo_columns mutates m_ice. At ncat==1 this is 0 or 1 (binary concentration). SCRATCH (recomputed every thermo step, only when has_snowfall) — zero-init, NOT restart-carried, same lifecycle as fb_part_sum.

real(kind=wp), public, allocatable :: snow_to_ice(:,:,:)

PR 27: Archimedes freeboard snow-ice conversion this thermo window (kg/m² per category), shape (nx_total, ny_total, ncat), >= 0 — SIS2 SN2IC. Filled by ice_thermo_columns (zeroed unconditionally, written only when snow_ice is on and the column floods). Same lifecycle as sw_thru: filled, not yet consumed by any diagnostic/coupling seam (a future diagnostics PR wires it in — sw_thru precedent). NOT restart-carried (window scratch).

real(kind=wp), public, allocatable :: sst_seam(:,:)

Sampled sea-surface temperature (degC) at the one-step-lagged surface, filled by ice_compute_basal_flux and reused by rdb_ice_thermo_driver — one sample, shared by fb and the column’s ocean-side inputs. SCRATCH.

real(kind=wp), public, allocatable :: ssurf_seam(:,:)

Sampled sea-surface salinity (PSU), same sharing contract as sst_seam. SCRATCH.

real(kind=wp), public, allocatable :: str_d(:,:)

Divergence stress tensor component [Pa*m], T-cells, shape (nx_total, ny_total). SIS2 CS%str_d. Persistent, restart- carried (optional). Relaxes to -0.5*pres_mice*mice at zero strain rate (the elliptical yield curve’s pressure intercept — ice-free cells have mice=0 so this is exactly 0 there, an emergent property, never a Dirichlet ice-edge special case).

real(kind=wp), public, allocatable :: str_s(:,:)

Shearing stress tensor component (cross term) [Pa*m], corners, shape (nx_total+1, ny_total+1). SIS2 CS%str_s. Persistent, restart-carried (optional).

real(kind=wp), public, allocatable :: str_t(:,:)

Tension stress tensor component [Pa*m], T-cells, shape (nx_total, ny_total). SIS2 CS%str_t. Persistent, restart- carried (optional).

real(kind=wp), public, allocatable :: sw_thru(:,:,:)

Shortwave transmitted through the ice to the ocean (W/m²), shape (nx_total, ny_total, ncat), >= 0. Per-category column output; PR 31 reduces it to the per-cell sw_thru_diag below (which IS coupled to the ocean surface-flux slot). SCRATCH (recomputed every thermo step, delete-mapped).

real(kind=wp), public, allocatable :: sw_thru_diag(:,:)

PR 31: ice -> ocean shortwave transmitted through the ice to the water below, reduced to per unit CELL area from the per-category sw_thru (W/m^2, >= 0, POSITIVE DOWN), shape (nx_total, ny_total). Refilled every thermo step by ice_thermo_driver_reduce{,_multicat}_impl (area-weighted at ncat>1, lumped at ncat==1 — the same convention as heat_flux_diag), consumed by ice_ocean_sw_flux (rdb_ice_ocean_coupler): with &ocean_forcing_nml enable_components on it fills the q_sw surface-flux component (assembler sums it into Q_heat); off, it is added directly into Q_heat. PERSISTENT + restart-carried (mirrors heat_flux_diag): the coupler rebuilds the ocean SW every thermo step from this and the driver’s resume fold re-applies it, so a resume does not cold-start the ice-SW contribution for one thermo window. Distinct from the per-category sw_thru above (SCRATCH, recomputed, delete-mapped).

real(kind=wp), public, allocatable :: tau_a_x(:,:)

Atmospheric (wind) stress felt by the ice, snapshotted onto u-faces [Pa], shape (nx_total+1, ny_total). Configure-time copy of the ocean’s wind-stress field (D7: one atmospheric stress field, no ice-specific bulk drag law) — NOT restart- carried (rebuilt at configure from the wind-stress config on every run, resumed or fresh).

real(kind=wp), public, allocatable :: tau_a_y(:,:)

Ditto, v-faces, shape (nx_total, ny_total+1).

real(kind=wp), public :: tau_ocn_valid = 0.0_wp

HOST-ONLY (PR 63). Exactly 0.0 or 1.0. 1.0 iff tau_ocn_x/tau_ocn_y hold a blend written by this run’s ice_ocean_stress_flux or restored from a checkpoint that carried one; 0.0 on a fresh run or a resume from a pre-PR-63 checkpoint. Set by ice_ocean_stress_flux, carried by register_scalar (which forces device_mapped=.false.), read only by ice_ocean_stress_resume_apply. NEVER device-mapped and NEVER read in a kernel — the SIS2 query_initialized('stress_mag') idiom (ice_model.F90:2410), one host scalar instead of a per-field registry query.

real(kind=wp), public, allocatable :: tau_ocn_x(:,:)

PR 63. Mirror of the exact value ice_ocean_stress_flux last wrote into ocean_surface_stress_t%tau_x [Pa], u-faces, shape (nx_total+1, ny_total). PERSISTENT, restart-carried (optional). Formula-agnostic by construction — it is a COPY of the blend’s output, never a recomputation, so it stays exact under any future change to the blend formula (SIS2 Ice%flux_u, ice_type.F90:242). Valid to read on the host only after !$acc update self of the COMPONENT array (never the aggregate ice — commit 72152870). See ice_ocean_stress_resume_apply (rdb_ice_ocean_coupler).

real(kind=wp), public, allocatable :: tau_ocn_y(:,:)

Ditto, v-faces, shape (nx_total, ny_total+1). SIS2 Ice%flux_v.

real(kind=wp), public, allocatable :: tfw_seam(:,:)

Sampled seawater freezing temperature at the surface (degC), same sharing contract as sst_seam. SCRATCH.

real(kind=wp), public, allocatable :: tr_flux_x_work(:,:,:)

Per-category zonal DONOR-VALUE buffer (val(donor of face I)), shape (nx_total+1, ny_total, ncat). GPU-race-free gather/scatter split (mirrors rdb_continuity’s Tr_face_left_x pattern): a first do concurrent over FACES reads val only at each face’s OWN donor cell and writes here (never the same cell two faces disagree on); the cell-update kernel then reads only this face buffer (to recover val_e/ val_w directly — NOT flux-weighted, so no back-division is needed) plus its OWN cell’s prior mass/value — no kernel ever reads a NEIGHBOUR cell’s val while another iteration writes that neighbour. Reused across every riding field (m_ice, each enth_ice/sal_ice layer, enth_snow) one at a time within a pass — SCRATCH.

real(kind=wp), public, allocatable :: tr_flux_y_work(:,:,:)

Meridional twin of tr_flux_x_work, shape (nx_total, ny_total+1, ncat).

logical, public :: transport = .false.

Horizontal category transport switch (&ocean_ice_nml transport), set from config BEFORE init (same convention as enable/ncat). Gates allocation of the transport-only workspace below (memory: the per-category face fluxes are ~4·ncat·N reals — do not pay it by default). u_ice/v_ice are unconditional (allocated whenever the slot is live) since PR 5’s EVP dynamics will need them regardless of whether category transport is on.

real(kind=wp), public, allocatable :: tsurf_out(:,:,:)

Column skin temperature (degC), shape (nx_total, ny_total, ncat) — diagnostic, recomputed every thermo step. SCRATCH.

real(kind=wp), public, allocatable :: u_ice(:,:)

Ice velocity at C-grid u-faces (m/s), shape (nx_total+1, ny_total). v1 INTERIM: sampled from the ocean surface layer every transport call (ice_transport_step Phase 0); PR 5’s EVP dynamics will write this field instead of the sampler, same faces. Persistent (unconditional allocation whenever the ice slot is live).

real(kind=wp), public, allocatable :: uh_ice(:,:,:)

Per-category zonal ice-mass face transport (kg/s), shape (nx_total+1, ny_total, ncat). SCRATCH, recomputed every pass.

real(kind=wp), public, allocatable :: uh_snow(:,:,:)

Per-category zonal snow-mass face transport (kg/s), shape (nx_total+1, ny_total, ncat). SCRATCH.

real(kind=wp), public, allocatable :: uhtot_work(:,:)

Category-SUMMED zonal face transport (kg/s), shape (nx_total+1, ny_total). SCRATCH, reused per medium.

real(kind=wp), public, allocatable :: v_ice(:,:)

Ice velocity at C-grid v-faces (m/s), shape (nx_total, ny_total+1). Same interim/persistent contract as u_ice.

real(kind=wp), public, allocatable :: vh_ice(:,:,:)

Per-category meridional ice-mass face transport (kg/s), shape (nx_total, ny_total+1, ncat). SCRATCH.

real(kind=wp), public, allocatable :: vh_snow(:,:,:)

Per-category meridional snow-mass face transport (kg/s), shape (nx_total, ny_total+1, ncat). SCRATCH.

real(kind=wp), public, allocatable :: vhtot_work(:,:)

Category-SUMMED meridional face transport (kg/s), shape (nx_total, ny_total+1). SCRATCH, reused per medium.


Type-Bound Procedures

procedure, public, non_overridable :: bytes => ocean_sea_ice_bytes

  • private pure function ocean_sea_ice_bytes(this) result(nbytes)

    Counted allocatable footprint of the sea-ice slot (one arr_bytes term per array, mirroring ocean_meke_bytes).

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_sea_ice_t), intent(in) :: this

    Return Value integer(kind=int64)

procedure, public, non_overridable :: destroy => ocean_sea_ice_destroy

  • private subroutine ocean_sea_ice_destroy(this)

    Reverse of init. Clears is_init first (use-after-destroy guard), then releases allocations.

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_sea_ice_t), intent(inout) :: this

procedure, public, non_overridable :: enter_data => ocean_sea_ice_enter_data

procedure, public, non_overridable :: exit_data => ocean_sea_ice_exit_data

procedure, public, non_overridable :: init => ocean_sea_ice_init

  • private subroutine ocean_sea_ice_init(this, grid)

    Cache the grid extents, allocate the frazil (PR 1) + Winton column (PR 3a) prognostics, and mark the slot live. Allocation is gated on enable at the parent call site (memory Rule 2), so an ice-off run never carries these arrays.

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_sea_ice_t), intent(inout) :: this
    type(hgrid_t), intent(in) :: grid

Source Code

   type :: ocean_sea_ice_t
      !! Sea-ice state slot.  All fields default to the inert
      !! (`enable=.false.`) configuration so an ocean run that never sets
      !! `&ocean_ice_nml` is byte-identical to a build without this slot.
      logical :: is_init = .false.
         !! True between `init` and `destroy`; gate on this (never on
         !! `allocated`, which misses the GPU mapping).
      logical :: enable = .false.
         !! Master switch (`&ocean_ice_nml enable`).  Off (default) ⇒ the
         !! parent state never inits / maps / steps this slot ⇒ no-op.
      integer :: ncat = 5
         !! Number of ice thickness categories (`&ocean_ice_nml ncat`).
         !! Category 0 is open water; SIS2 default 5.  Sizes the
         !! per-category arrays when PR 3+ adds them.
      integer :: nk_ice = 2
         !! Vertical ice layers per category (`&ocean_ice_nml nk_ice`).
         !! 2 = Winton two-layer (v1).  Bottom-up once arrays land
         !! (k=1 ice bottom — SIS2's top-down index is flipped at this
         !! state layer, per the plan).
      logical :: transport = .false.
         !! Horizontal category transport switch (`&ocean_ice_nml
         !! transport`), set from config BEFORE `init` (same convention as
         !! `enable`/`ncat`).  Gates allocation of the transport-only
         !! workspace below (memory: the per-category face fluxes are
         !! ~4·ncat·N reals — do not pay it by default).  `u_ice`/`v_ice`
         !! are unconditional (allocated whenever the slot is live) since
         !! PR 5's EVP dynamics will need them regardless of whether
         !! category transport is on.
      logical :: dynamics = .false.
         !! EVP rheology master switch (`&ocean_ice_nml dynamics`), set
         !! from config BEFORE `init` (same convention as `enable`/
         !! `transport`). Gates the driver's `ice_evp_step` call and the
         !! transport sampler skip; does NOT gate any allocation here —
         !! `str_d`/`str_t`/`str_s`/`tau_a_*`/`fxoc`/`fyoc`/`tau_ocn_*`
         !! are unconditional (cheap 2-D arrays), same contract as
         !! `u_ice`/`v_ice`.
      logical :: has_snowfall = .false.
         !! Host-side dispatch gate for the PR 26 snowfall source term,
         !! latched from `(cfg%ocean%ice%snowfall /= 0.0_wp)` BEFORE `init`
         !! (same convention as `transport`/`dynamics`).  Gates the
         !! pre-column `snow_part_ocn` fill and the `ice_snowfall_ocean_
         !! share` contributor call; does NOT gate allocation of `atm_fprec`/
         !! `snow_part_ocn`/`fprec_ocn_diag` (cheap 2-D arrays, same
         !! contract as `atm_sw_dn`/`fb_part_sum`) — the `has_sw`/`has_heat`
         !! idiom that makes `snowfall=0` byte-identical by construction,
         !! not by IEEE argument.
      real(wp) :: tau_ocn_valid = 0.0_wp
         !! HOST-ONLY (PR 63). Exactly 0.0 or 1.0. `1.0` iff
         !! `tau_ocn_x`/`tau_ocn_y` hold a blend written by this run's
         !! `ice_ocean_stress_flux` or restored from a checkpoint that
         !! carried one; `0.0` on a fresh run or a resume from a
         !! pre-PR-63 checkpoint. Set by `ice_ocean_stress_flux`, carried
         !! by `register_scalar` (which forces `device_mapped=.false.`),
         !! read only by `ice_ocean_stress_resume_apply`. NEVER
         !! device-mapped and NEVER read in a kernel — the SIS2
         !! `query_initialized('stress_mag')` idiom
         !! (`ice_model.F90:2410`), one host scalar instead of a
         !! per-field registry query.
      logical :: snow_ice = .false.
         !! Archimedes freeboard snow-ice flood master switch
         !! (`&ocean_ice_nml snow_ice`, PR 27), latched from config
         !! BEFORE `init` (same convention as `transport`/`dynamics`).
         !! Read by `ice_thermo_driver_step` and passed to
         !! `ice_thermo_columns` as `do_snow_ice`; does NOT gate
         !! allocation of `snow_to_ice` (cheap per-category array, same
         !! contract as `sw_thru`) — `.false.` (default) is what makes
         !! `snow_ice=.false.` byte-identical by construction.

      real(wp), allocatable :: u_ice(:, :)
         !! Ice velocity at C-grid u-faces (m/s), shape (nx_total+1,
         !! ny_total).  v1 INTERIM: sampled from the ocean surface layer
         !! every transport call (`ice_transport_step` Phase 0); PR 5's
         !! EVP dynamics will write this field instead of the sampler, same
         !! faces.  Persistent (unconditional allocation whenever the ice
         !! slot is live).
      real(wp), allocatable :: v_ice(:, :)
         !! Ice velocity at C-grid v-faces (m/s), shape (nx_total,
         !! ny_total+1).  Same interim/persistent contract as `u_ice`.

      ! ---- PR 5: C-grid EVP ice dynamics ----
      real(wp), allocatable :: str_d(:, :)
         !! Divergence stress tensor component [Pa*m], T-cells, shape
         !! (nx_total, ny_total). SIS2 `CS%str_d`. Persistent, restart-
         !! carried (optional). Relaxes to `-0.5*pres_mice*mice` at zero
         !! strain rate (the elliptical yield curve's pressure intercept —
         !! ice-free cells have `mice=0` so this is exactly 0 there, an
         !! emergent property, never a Dirichlet ice-edge special case).
      real(wp), allocatable :: str_t(:, :)
         !! Tension stress tensor component [Pa*m], T-cells, shape
         !! (nx_total, ny_total). SIS2 `CS%str_t`. Persistent, restart-
         !! carried (optional).
      real(wp), allocatable :: str_s(:, :)
         !! Shearing stress tensor component (cross term) [Pa*m],
         !! corners, shape (nx_total+1, ny_total+1). SIS2 `CS%str_s`.
         !! Persistent, restart-carried (optional).
      real(wp), allocatable :: tau_a_x(:, :)
         !! Atmospheric (wind) stress felt by the ice, snapshotted onto
         !! u-faces [Pa], shape (nx_total+1, ny_total). Configure-time
         !! copy of the ocean's wind-stress field (D7: one atmospheric
         !! stress field, no ice-specific bulk drag law) — NOT restart-
         !! carried (rebuilt at configure from the wind-stress config on
         !! every run, resumed or fresh).
      real(wp), allocatable :: tau_a_y(:, :)
         !! Ditto, v-faces, shape (nx_total, ny_total+1).
      real(wp), allocatable :: fxoc(:, :)
         !! Subcycle-averaged ice->ocean stress [Pa], u-faces, shape
         !! (nx_total+1, ny_total). SIS2 `fxoc`. Persistent, restart-
         !! carried (optional): `ice_ocean_stress_flux` blends this into
         !! `ocean_surface_stress_t%tau_x` every outer step. PR 63:
         !! this registration is no longer what makes the resume
         !! bit-exact — `tau_ocn_x`/`tau_ocn_y` below carry the blend's
         !! OUTPUT directly, so the resume path never needs to
         !! reconstruct it from `fxoc` + the checkpointed `ci`. Kept
         !! restart-carried for diagnostic continuity across a resume
         !! (like `m_frozen_diag`/`m_melt_diag`) — its only cross-step
         !! reader inside a call is `ice_evp_dynamics`'s own zeroing at
         !! entry, so it carries no state INTO the EVP solve either.
      real(wp), allocatable :: fyoc(:, :)
         !! Ditto, v-faces, shape (nx_total, ny_total+1). SIS2 `fyoc`.
      real(wp), allocatable :: tau_ocn_x(:, :)
         !! PR 63. Mirror of the exact value `ice_ocean_stress_flux` last
         !! wrote into `ocean_surface_stress_t%tau_x` [Pa], u-faces, shape
         !! (nx_total+1, ny_total). PERSISTENT, restart-carried
         !! (optional). Formula-agnostic by construction — it is a COPY
         !! of the blend's output, never a recomputation, so it stays
         !! exact under any future change to the blend formula (SIS2
         !! `Ice%flux_u`, `ice_type.F90:242`). Valid to read on the host
         !! only after `!$acc update self` of the COMPONENT array (never
         !! the aggregate `ice` — commit `72152870`). See
         !! `ice_ocean_stress_resume_apply` (`rdb_ice_ocean_coupler`).
      real(wp), allocatable :: tau_ocn_y(:, :)
         !! Ditto, v-faces, shape (nx_total, ny_total+1). SIS2
         !! `Ice%flux_v`.

      type(evp_workspace_t) :: evp_ws
         !! Persistent EVP-dynamics scratch (`rdb_ice_evp`'s subcycle
         !! kernels).  Allocated + GPU-mapped ONLY when `dynamics` is on
         !! (see `evp_workspace_t`) — the successor to the retired
         !! `rdb_ice_evp` module-level `save` workspace.  Rides
         !! `ocean_state_enter_data` via this slot's `enter_data`.

      ! ---- PR 4b: category transport workspace (allocated iff `transport`) ----
      real(wp), allocatable :: mca_ice(:, :, :)
         !! Cell-averaged ice mass per category (CAS space, kg/m² of CELL
         !! area — SIS2 `CAS%m_ice`), shape (nx_total, ny_total, ncat).
         !! SCRATCH: filled at Phase 1 (IST->CAS), consumed/updated through
         !! Phase 2, discarded after Phase 3 (CAS->IST).
      real(wp), allocatable :: mca_snow(:, :, :)
         !! Cell-averaged snow mass per category (CAS space, kg/m² of CELL
         !! area), shape (nx_total, ny_total, ncat).  Same lifecycle as
         !! `mca_ice`.
      real(wp), allocatable :: uh_ice(:, :, :)
         !! Per-category zonal ice-mass face transport (kg/s), shape
         !! (nx_total+1, ny_total, ncat).  SCRATCH, recomputed every pass.
      real(wp), allocatable :: vh_ice(:, :, :)
         !! Per-category meridional ice-mass face transport (kg/s), shape
         !! (nx_total, ny_total+1, ncat).  SCRATCH.
      real(wp), allocatable :: uh_snow(:, :, :)
         !! Per-category zonal snow-mass face transport (kg/s), shape
         !! (nx_total+1, ny_total, ncat).  SCRATCH.
      real(wp), allocatable :: vh_snow(:, :, :)
         !! Per-category meridional snow-mass face transport (kg/s), shape
         !! (nx_total, ny_total+1, ncat).  SCRATCH.
      real(wp), allocatable :: htot_work(:, :)
         !! Category-SUMMED mass, 2-D scratch (kg/m² of CELL area), shape
         !! (nx_total, ny_total).  Reused for the ice solve then the snow
         !! solve, per direction (SIS2 `zonal_mass_flux`'s `htot`).
      real(wp), allocatable :: hl_x_work(:, :)
         !! PPM face-edge reconstruction of the category-SUMMED zonal mass,
         !! FACE-shaped `(nx_total+1, ny_total)` (mirrors `rdb_continuity`'s
         !! `h_face_left_x`).  `hl_x_work(i,j)` is the value AT east face
         !! `i` extrapolated from the LEFT cell `(i-1)` (i.e. cell i-1's
         !! own right/downwind edge); `hr_x_work(i,j)` is AT face `i` from
         !! the RIGHT cell `i` (cell i's own left edge).  FACE shape is
         !! load-bearing: the array-edge fallback writes index `nx+1`
         !! (out of bounds for a `(nx,ny)` array — the pre-fix bug).
         !! SCRATCH, reused per medium.
      real(wp), allocatable :: hr_x_work(:, :)
         !! Right-cell zonal face-edge companion to `hl_x_work`,
         !! `(nx_total+1, ny_total)`.  SCRATCH.
      real(wp), allocatable :: hl_y_work(:, :)
         !! North-face PPM edge, `(nx_total, ny_total+1)` — the meridional
         !! twin of `hl_x_work` (`h_face_left_y` convention). SCRATCH.
      real(wp), allocatable :: hr_y_work(:, :)
         !! North-face right-cell PPM edge, `(nx_total, ny_total+1)`.
         !! SCRATCH.
      real(wp), allocatable :: uhtot_work(:, :)
         !! Category-SUMMED zonal face transport (kg/s), shape
         !! (nx_total+1, ny_total).  SCRATCH, reused per medium.
      real(wp), allocatable :: vhtot_work(:, :)
         !! Category-SUMMED meridional face transport (kg/s), shape
         !! (nx_total, ny_total+1).  SCRATCH, reused per medium.
      real(wp), allocatable :: tr_flux_x_work(:, :, :)
         !! Per-category zonal DONOR-VALUE buffer (`val(donor of face
         !! I)`), shape (nx_total+1, ny_total, ncat).  GPU-race-free
         !! gather/scatter split (mirrors `rdb_continuity`'s
         !! `Tr_face_left_x` pattern): a first `do concurrent` over FACES
         !! reads `val` only at each face's OWN donor cell and writes here
         !! (never the same cell two faces disagree on); the cell-update
         !! kernel then reads only this face buffer (to recover `val_e`/
         !! `val_w` directly — NOT flux-weighted, so no back-division is
         !! needed) plus its OWN cell's prior mass/value — no kernel ever
         !! reads a NEIGHBOUR cell's `val` while another iteration writes
         !! that neighbour.  Reused across every riding field (`m_ice`,
         !! each `enth_ice`/`sal_ice` layer, `enth_snow`) one at a time
         !! within a pass — SCRATCH.
      real(wp), allocatable :: tr_flux_y_work(:, :, :)
         !! Meridional twin of `tr_flux_x_work`, shape (nx_total,
         !! ny_total+1, ncat).

      real(wp), allocatable :: frazil_heat(:, :)
         !! Ocean-side frazil supercooling BANK (J/m², T-cells, shape
         !! (nx_total, ny_total)).  `ice_frazil_accumulate` clamps the
         !! surface layer at the freezing point and deposits the removed
         !! heat deficit `ρ·Cp·h·(T_f − T)⁺` here; the ice thermodynamics
         !! (PR 3) will SPEND it as new-ice formation.  Persistent state
         !! (accumulates across steps, restart-carried) — distinct from
         !! the drain-and-zero budget accumulator below.
      real(wp), allocatable :: heat_budget_frazil(:, :, :)
         !! hTr (K·m) change per cell per outer step attributed to the
         !! frazil clamp — the `budgets` HEAT contributor mirroring the
         !! `heat_budget_surface` convention (positive = heat added to
         !! the ocean; the clamp WARMS the surface layer up to T_f).
         !! Shape (nx_total, ny_total, 1): the clamp only ever touches
         !! the surface layer, and the drain integrates over k anyway.
         !! Drained-and-zeroed by `ocean_budgets_drain_contributors`.

      ! ---- PR 3a: Winton column prognostics ----
      real(wp), allocatable :: part_size(:, :, :)
         !! Fractional ice-category area, shape (nx_total, ny_total,
         !! 0:ncat). Category 0 = OPEN WATER; Σ_cat part_size = 1.
         !! TWO-MODE CONVENTION (module docstring): `ncat==1` — legacy
         !! lumped mode, deliberately UNTOUCHED by every kernel
         !! (`part_size(:,:,0)=1` forever, category 1 unused, thermo runs
         !! per unit CELL area). `ncat>1` — SIS2 ITD mode (PR 4a), LIVE:
         !! `ice_frazil_uptake_multicat_impl` annexes open water into an
         !! occupied category, and `ice_adjust_categories` restores the
         !! per-cell partition each thermo window.
      real(wp), allocatable :: m_ice(:, :, :)
         !! Total ice mass per category (kg/m²), shape (nx_total,
         !! ny_total, ncat). TWO-MODE CONVENTION: `ncat==1` — per unit
         !! CELL area (legacy). `ncat>1` — per unit ICE-COVERED area
         !! (SIS2 `mH_ice`, intensive quantity DIVIDED by `part_size`,
         !! not multiplied). Layer masses are always `m_ice/nk_ice` in
         !! EITHER mode — the column step always ends with
         !! `ice_rebalance_layers`, so equal-mass layering is a state
         !! invariant regardless of the area convention.
      real(wp), allocatable :: m_snow(:, :, :)
         !! Snow mass per category (kg/m²), shape (nx_total, ny_total,
         !! ncat). Same two-mode area convention as `m_ice` (per-CELL at
         !! ncat==1, per-ICE-area at ncat>1).
      real(wp), allocatable :: enth_ice(:, :, :, :)
         !! Ice specific enthalpy (J/kg), shape (nx_total, ny_total,
         !! ncat, nk_ice). BOTTOM-UP: k=1 = ice BOTTOM (ocean side),
         !! k=nk_ice = ice TOP (atm/snow side) — the opposite of the
         !! SIS2/`rdb_ice_column` internal top-down convention; the
         !! flip happens at the `ice_column_step` gather/scatter
         !! boundary (`rdb_ice_column`'s module docstring, TRAP #2).
      real(wp), allocatable :: enth_snow(:, :, :, :)
         !! Snow specific enthalpy (J/kg), shape (nx_total, ny_total,
         !! ncat, 1).
      real(wp), allocatable :: sal_ice(:, :, :, :)
         !! Ice bulk salinity (PSU), shape (nx_total, ny_total, ncat,
         !! nk_ice), same BOTTOM-UP k convention as `enth_ice`.

      ! ---- PR 3b: frazil->ice uptake + brine-rejection coupling ----
      real(wp), allocatable :: m_frozen_diag(:, :)
         !! New frazil-ice mass formed in the LAST thermo window (kg per
         !! m² of CELL area — v1 category convention, see
         !! `rdb_ice_frazil_uptake`), shape (nx_total, ny_total).
         !! OVERWRITTEN (not accumulated) each uptake; zero where no
         !! freezing occurred. Restart-carried for post-resume
         !! diagnostic continuity.
      real(wp), allocatable :: salt_flux_diag(:, :)
         !! Ice -> ocean virtual salt flux from the last uptake
         !! (PSU·kg/m²/s, POSITIVE SALINIFIES — the surface-flux Q_salt
         !! convention), shape (nx_total, ny_total). PERSISTENT +
         !! restart-carried: `rdb_ice_ocean_coupler` refills `Q_salt`
         !! from this field each thermo step, and the driver's
         !! configure-time resume fold re-applies it so the first
         !! post-resume window sees the same flux the uninterrupted run
         !! would have. PR 3c: the column driver ADDS its net-melt
         !! contribution here on top of what the frazil uptake (which
         !! runs first each window) wrote — see `rdb_ice_thermo_driver`.

      ! ---- PR 3c: coupleable atmospheric-forcing seam ----
      real(wp), allocatable :: atm_sf0(:, :)
         !! Net upward surface flux at T_surf=0 (W/m^2), the SEB intercept
         !! `sf_0` fed to ice_thermo_columns. Filled every thermo step by
         !! ice_atm_forcing_restoring (v1) or the future bulk-flux subsystem.
         !! SCRATCH (recomputed each step) — NOT restart-carried.
      real(wp), allocatable :: atm_dsfdt(:, :)
         !! SEB slope dSF/dT (W/m^2/K) — `dsf_dt`. Same seam contract.
      real(wp), allocatable :: atm_sw_dn(:, :)
         !! Downwelling shortwave into the ice top (W/m^2) — `sw_dn`. Same.
      real(wp), allocatable :: atm_fprec(:, :)
         !! PR 26: frozen-precipitation rate onto the ice top (kg/m^2/s),
         !! >= 0 — the seam `ice_atm_forcing_restoring` fills from
         !! `&ocean_ice_nml snowfall` exactly like `atm_sw_dn` <- `sw_down`.
         !! Same SCRATCH contract (refilled every thermo step, NOT restart-
         !! carried). PR 55 fills this from data instead of a uniform
         !! scalar; the shape/units/lifecycle are fixed by this PR (see
         !! `rdb_ice_snow`'s module docstring for the binding seam spec).

      ! ---- PR 3c: ocean -> ice basal flux + the sample seam it shares
      !       with the column driver ----
      real(wp), allocatable :: fb(:, :)
         !! Ocean -> ice-base heat flux (W/m^2), filled by
         !! `rdb_ice_basal_flux%ice_compute_basal_flux` from the
         !! above-freezing SST. SCRATCH (recomputed each step) — NOT
         !! restart-carried. Same lifecycle as the `atm_*` seam.
      real(wp), allocatable :: sst_seam(:, :)
         !! Sampled sea-surface temperature (degC) at the one-step-lagged
         !! surface, filled by `ice_compute_basal_flux` and reused by
         !! `rdb_ice_thermo_driver` — one sample, shared by `fb` and the
         !! column's ocean-side inputs. SCRATCH.
      real(wp), allocatable :: ssurf_seam(:, :)
         !! Sampled sea-surface salinity (PSU), same sharing contract as
         !! `sst_seam`. SCRATCH.
      real(wp), allocatable :: tfw_seam(:, :)
         !! Sampled seawater freezing temperature at the surface (degC),
         !! same sharing contract as `sst_seam`. SCRATCH.

      ! ---- PR 3c: column per-category scratch outputs ----
      real(wp), allocatable :: tsurf_out(:, :, :)
         !! Column skin temperature (degC), shape (nx_total, ny_total,
         !! ncat) — diagnostic, recomputed every thermo step. SCRATCH.
      real(wp), allocatable :: h2o_ocn_to_ice(:, :, :)
         !! Mass FROZEN from the ocean onto the ice base (kg/m²), shape
         !! (nx_total, ny_total, ncat), >= 0. SCRATCH.
      real(wp), allocatable :: h2o_ice_to_ocn(:, :, :)
         !! Meltwater mass to the ocean (kg/m²), shape (nx_total,
         !! ny_total, ncat), >= 0. SCRATCH.
      real(wp), allocatable :: heat_to_ocn(:, :, :)
         !! Leftover melt energy drained back to the ocean (J/m²), shape
         !! (nx_total, ny_total, ncat), >= 0. SCRATCH.
      real(wp), allocatable :: sw_thru(:, :, :)
         !! Shortwave transmitted through the ice to the ocean (W/m²),
         !! shape (nx_total, ny_total, ncat), >= 0. Per-category column
         !! output; PR 31 reduces it to the per-cell `sw_thru_diag` below
         !! (which IS coupled to the ocean surface-flux slot). SCRATCH
         !! (recomputed every thermo step, `delete`-mapped).
      real(wp), allocatable :: snow_to_ice(:, :, :)
         !! PR 27: Archimedes freeboard snow-ice conversion this thermo
         !! window (kg/m² per category), shape (nx_total, ny_total,
         !! ncat), >= 0 — SIS2 `SN2IC`. Filled by `ice_thermo_columns`
         !! (zeroed unconditionally, written only when `snow_ice` is on
         !! and the column floods). Same lifecycle as `sw_thru`: filled,
         !! not yet consumed by any diagnostic/coupling seam
         !! (a future diagnostics PR wires it in — `sw_thru` precedent).
         !! NOT restart-carried (window scratch).

      ! ---- PR 3c: melt-side ocean coupling diags ----
      real(wp), allocatable :: heat_flux_diag(:, :)
         !! Ice -> ocean net surface heat flux from the last thermo window
         !! (W/m^2, POSITIVE DOWN into the ocean — the Q_heat convention),
         !! shape (nx_total, ny_total). Q_heat = heat_to_ocn/dt_therm - fb
         !! (see `rdb_ice_thermo_driver`). PERSISTENT + restart-carried
         !! (mirrors salt_flux_diag): `rdb_ice_ocean_coupler` refills
         !! Q_heat from this each thermo step, and the driver resume fold
         !! re-applies it.  This is the NON-shortwave heat share only — the
         !! penetrating shortwave `sw_thru_diag` below is a SEPARATE field
         !! (PR 31), never folded in here (that would double-count against
         !! the `q_sw` component / the components-off Q_heat fold — see
         !! `ice_ocean_sw_flux` in `rdb_ice_ocean_coupler`).
      real(wp), allocatable :: sw_thru_diag(:, :)
         !! PR 31: ice -> ocean shortwave transmitted through the ice to
         !! the water below, reduced to per unit CELL area from the
         !! per-category `sw_thru` (W/m^2, >= 0, POSITIVE DOWN), shape
         !! (nx_total, ny_total).  Refilled every thermo step by
         !! `ice_thermo_driver_reduce{,_multicat}_impl` (area-weighted at
         !! ncat>1, lumped at ncat==1 — the same convention as
         !! `heat_flux_diag`), consumed by `ice_ocean_sw_flux`
         !! (`rdb_ice_ocean_coupler`): with `&ocean_forcing_nml
         !! enable_components` on it fills the `q_sw` surface-flux
         !! component (assembler sums it into `Q_heat`); off, it is added
         !! directly into `Q_heat`.  PERSISTENT + restart-carried (mirrors
         !! `heat_flux_diag`): the coupler rebuilds the ocean SW every
         !! thermo step from this and the driver's resume fold re-applies
         !! it, so a resume does not cold-start the ice-SW contribution for
         !! one thermo window.  Distinct from the per-category `sw_thru`
         !! above (SCRATCH, recomputed, `delete`-mapped).
      real(wp), allocatable :: m_melt_diag(:, :)
         !! Net meltwater mass to the ocean in the last window (kg/m^2 of
         !! cell, = h2o_ice_to_ocn - h2o_ocn_to_ice summed over cats;
         !! POSITIVE = net melt). OVERWRITTEN each window. Restart-carried
         !! for post-resume diagnostic continuity (like m_frozen_diag).
      real(wp), allocatable :: fprec_ocn_diag(:, :)
         !! PR 26: frozen precipitation delivered directly to the ocean
         !! (kg/m^2/s, per unit CELL area, >= 0) — the share of
         !! `atm_fprec` that lands where there is no ice
         !! (`snow_part_ocn`-weighted; open water at ncat>1, ice-free cells
         !! at ncat==1). Zero on land and on fully ice-covered cells.
         !! SCRATCH: zeroed + rewritten every thermo window by
         !! `ice_snowfall_ocean_share`, NOT restart-carried. Binding seam
         !! spec (consumed by PR-16 as a `net_massin` source): see
         !! `rdb_ice_snow`'s module docstring.

      ! ---- PR 4a: multi-category ITD ----
      real(wp), allocatable :: h_lim(:)
         !! Category lower thickness limits (m), shape (1:ncat+1) — SIS2
         !! `cat_thick_lim` (`SIS_state_initialization.F90:45-78`).
         !! `h_lim(c)` is the lower bound of category `c`; `h_lim(ncat+1)`
         !! is stored but never used as an upper cap (category `ncat` is
         !! unbounded above — SIS2 keeps it too). Computed at `init` by
         !! `ice_itd_category_bounds` from `ncat` and (PR-58) the optional
         !! `hlim_cfg` override below — NOT
         !! restart-carried (cheap to recompute, and doing so avoids a
         !! stale-`ncat` mismatch after a restart that changes `ncat`).
      real(wp), allocatable :: mh_lim(:)
         !! Category lower MASS limits (kg/m²), shape (1:ncat+1),
         !! `= ICE_RHO_ICE*h_lim` — SIS2 `mH_cat_bound`
         !! (`SIS_state_initialization.F90:75-77`). `ice_adjust_categories`
         !! compares `m_ice` against THIS array (mass space, like SIS2),
         !! never against `h_lim` directly. Same non-restart-carried
         !! lifecycle as `h_lim`.
      real(wp), allocatable :: hlim_cfg(:)
         !! PR-58: HOST-ONLY config latch: the user's `&ocean_ice_nml
         !! hlim` list, trimmed to its supplied length, set by
         !! `ocean_state_init_from_config` BEFORE `init`. Unallocated =>
         !! the SIS2 default table. NOT device-mapped (no kernel reads
         !! it — `h_lim`/`mh_lim` are the mapped artefacts, computed at
         !! `init` from this before `enter_data`) and NOT
         !! restart-carried (same lifecycle as `h_lim`).
      real(wp), allocatable :: fb_part_sum(:, :)
         !! Pre-column fb-charged ice-cover fraction snapshot (shape
         !! (nx_total, ny_total)) — `Σ_c part_size(c)` over the categories
         !! that pass the column's OWN entry gate
         !! (`m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED`), evaluated
         !! POST-uptake / PRE-column by `rdb_ice_thermo_driver` so the
         !! weight matches exactly the categories the column charges `fb`
         !! to before `ice_thermo_columns` mutates `m_ice`. SCRATCH
         !! (recomputed every thermo step at ncat>1 only) — zero-init,
         !! NOT restart-carried, same lifecycle as the `atm_*`/`fb`
         !! seam fields.
      real(wp), allocatable :: snow_part_ocn(:, :)
         !! PR 26: pre-column ice-free-cover fraction snapshot (shape
         !! (nx_total, ny_total)), `1 - Σ_c part_size(c)` over the
         !! categories that pass the column's OWN entry gate
         !! (`m_ice(i,j,c) > ICE_RHO_ICE*H_VANISHED`), evaluated PRE-column
         !! by `ice_snow_part_ocn_fill_impl` for the same reason as
         !! `fb_part_sum` — the weight must match the cover snow actually
         !! caught, before `ice_thermo_columns` mutates `m_ice`. At ncat==1
         !! this is 0 or 1 (binary concentration). SCRATCH (recomputed
         !! every thermo step, only when `has_snowfall`) — zero-init, NOT
         !! restart-carried, same lifecycle as `fb_part_sum`.

      ! ---- Cached extents ----
      integer :: nx_total = 0
      integer :: ny_total = 0
   contains
      procedure, non_overridable :: init => ocean_sea_ice_init
      procedure, non_overridable :: destroy => ocean_sea_ice_destroy
      procedure, non_overridable :: enter_data => ocean_sea_ice_enter_data
      procedure, non_overridable :: exit_data => ocean_sea_ice_exit_data
      procedure, non_overridable :: bytes => ocean_sea_ice_bytes
   end type ocean_sea_ice_t