ocean_surface_flux_t Derived Type

type, public :: ocean_surface_flux_t


Inherited by

type~~ocean_surface_flux_t~~InheritedByGraph type~ocean_surface_flux_t ocean_surface_flux_t type~ocean_state_t ocean_state_t type~ocean_state_t->type~ocean_surface_flux_t surface_flux 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 :: Q_heat(:,:)

2D net surface heat flux (W/m^2, positive downward), shape (nx, ny). Fill via set_surface_flux_const for spatially-uniform forcing (the default); Area-A3 override or Area-A4 restoring writes the field directly.

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

Scalar fill source for Q_heat(:,:). Seeded from &ocean_thermo_nml q_heat by set_surface_flux_const. Kept for diagnostic logging; kernels read Q_heat directly.

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

2D net surface salt flux (kg salt/m^2/s, positive salinifies), shape (nx, ny).

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

Scalar fill source for Q_salt(:,:). Seeded from &ocean_thermo_nml q_salt by set_surface_flux_const.

real(kind=wp), public :: cp = SEAWATER_CP

Specific heat capacity (J/kg/K).

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

Evaporative mass flux (kg/m^2/s, <= 0 — MOM6 convention, (-1)*flux out of the ocean). v1: enthalpy + salt bookkeeping only — does NOT change column mass (real freshwater is a named follow-up, see the module docstring).

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

Frozen precipitation / snowfall (kg/m^2/s, >= 0).

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

Frozen (calving/ice) runoff (kg/m^2/s, >= 0).

real(kind=wp), public :: h_min = 1.0e-3_wp

Floor on the surface-layer thickness in the 1/h_top division — keeps the kernel finite when the top layer pinches out.

logical, public :: has_heat = .false.

True when Q_heat carries a non-zero fill (set by set_surface_flux_const when q_heat_val /= 0). Any future field-fill path (A3 data-override, A4 restoring) MUST set has_heat = .true. after writing into Q_heat so the apply-tracers kernel fires. Host-side flag only (early-return guard in ocean_surface_flux_apply_tracers).

logical, public :: has_mass_flux = .false.

Host-side latch — set by a filler after writing ANY of evap/lprec/fprec/vprec/lrunoff/frunoff/ seaice_melt. Cheap early-return gate for a future freshwater kernel (real-mass PR). Same contract as has_heat (:56-62) — never set from a device reduction.

logical, public :: has_q_sw = .false.

Host-side latch — set by a filler after writing q_sw (e.g. an ice sw_thru coupler). Not the same as has_sw below (that gates shortwave penetration, an unrelated pre-existing switch) — do not conflate the two.

logical, public :: has_restore_S = .false.

True when SSS restoring is active (enable_restore_salt .and. restore_piston_S /= 0).

logical, public :: has_restore_T = .false.

True when SST restoring is active (enable_restore_temp .and. restore_piston_T /= 0). Host-side gate only — ocean_surface_restore_apply_tracers early-returns unless this or has_restore_S is set, so the default-off path is byte-for-byte unchanged.

logical, public :: has_salt = .false.

True when Q_salt carries a non-zero fill (set by set_surface_flux_const when q_salt_val /= 0). Same contract as has_heat for any field-fill path.

logical, public :: has_sw = .false.

True when shortwave penetration is active (set by set_sw_penetration when sw_pen_frac /= 0). Host-side gate only — ocean_surface_flux_apply_sw_penetration early-returns unless this is set, so the default-off path leaves the surface-flux deposition byte-for-byte unchanged.

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

Restoring / flux-adjustment / “other” net heat term not decomposed into the radiative/turbulent bands above (W/m^2, either sign; MOM6 heat_added). The v1 ice coupler’s heat_flux_diag lands here (§5.4 of the PR-12 plan) — it is already a net W/m^2, not further decomposable.

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

Ice-shelf cavity basal-melt heat component (W/m^2, same positive-DOWN-into-the-ocean convention as every other heat band; &ocean_cavity_melt_nml). OWNED by rdb_ocean_cavity_flux; written heat_cavity = -q_ocean, where q_ocean = rho_w*c_w*gamma_t*(T_w - T_b) > 0 is the kernel’s turbulent heat flux OCEAN -> INTERFACE, so warm water under a shelf COOLS the top of the column. It is a SEPARATE field from heat_added precisely because the sea-ice coupler full-overwrites heat_added — two writers on one slot clobber silently (cavity x sea ice is refused today, but the ownership rule must not depend on that). Zero unless a cavity melt step ran.

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

Enthalpy carried by fprec (W/m^2).

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

Enthalpy carried by frunoff (W/m^2).

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

Enthalpy carried by lprec (W/m^2). A filler that writes lprec MUST fill this — the v1 convenience is SEAWATER_CP * T_source * lprec. The source (not the ocean) owns this enthalpy — see the module docstring §(d).

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

Enthalpy carried by lrunoff (W/m^2).

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

Assembler output — do NOT write. Sum of the six heat_content_<flux> companions above (W/m^2, >= 0 for warm inflow). Filled by ocean_surface_flux_assemble.

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

Assembler output — do NOT write. SEAWATER_CP * T_sst * evap (W/m^2, <= 0 since evap <= 0) — the enthalpy the ocean loses with evaporating mass, computed from the ocean’s own surface temperature (the ocean, not a filler, owns this number). There is deliberately NO heat_content_evap field — see the PR-12 plan §11.6. Filled by ocean_surface_flux_assemble.

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

Enthalpy carried by seaice_melt (W/m^2).

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

Enthalpy carried by vprec (W/m^2).

logical, public :: is_init = .false.

True between init and destroy.

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

Liquid precipitation (kg/m^2/s, >= 0 into the ocean).

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

Liquid river runoff (kg/m^2/s, >= 0).

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

Assembled total (Pa, >= 0) — p_surf_atm plus any ice mass-loading term, full overwrite, never += (a += ratchets the load across outer steps with no bound). No consumer in this PR; ships zeroed alongside p_surf_atm so the follow-up PGF fold needs no further plumbing.

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

Input component. Atmospheric surface-pressure load (Pa, >= 0). Filled by an external reader / configure-time scalar seed — the sea-ice path never writes this field. Ships zeroed with no consumer in this PR (the inverse- barometer PGF fold is a same-release-cycle follow-up).

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

Host latch, 1 once ocean_surface_flux_assemble has derived Q_heat/Q_salt from the component set. From then on the two arrays are CARRIED state: the assembler runs at the END of a thermo step and the steps up to the next one read what it left (SST-dependent terms included), so with use_components they are checkpointed, and this latch tells a warm restart that the checkpointed arrays are an assembly to resume from (0 => an older checkpoint or none yet: the configure-time seed stands). A real, not a logical, because the restart registry carries real scalars.

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

Latent heat flux (W/m^2, typically < 0, positive down).

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

Net longwave (W/m^2, typically < 0, positive down).

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

Sensible heat flux (W/m^2, typically < 0, positive down).

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

Shortwave into the ocean (W/m^2, >= 0, positive down).

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

Scalar target SSS (PSU).

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

Scalar target SST (degC). Read by-value into the device _impl kernel — no per-cell field (so no extra device array, the enter_data orchestrator is untouched). A 2D-field target is the documented A4-v2 follow-up.

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

SSS piston velocity (m/s).

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

SST piston velocity (m/s), seeded from &ocean_restore_nml piston_t (m/day) via /86400. The surface relaxation rate for a top layer of thickness h_top is lambda = restore_piston_T / h_top [1/s].

real(kind=wp), public :: rho0 = 1035.0_wp

Boussinesq reference density (kg/m^3) — the dt/(rho0*cp) heat and dt/rho0 salt divisors applied to EVERY surface tracer source, including whatever the sea-ice coupler writes into Q_heat/Q_salt.

ASSIGNED FROM CONFIG by configure_ocean_reference_density, which copies the single rho0 of record (&ocean_ic_nml rho_0 -> eos%rho0). The literal here is only the pre-configure type default; do not read it as the value a run uses. Host scalar: the divisor is folded into the inv_scale argument on the host, so the assignment owes no !$acc update device.

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

Ice-shelf cavity basal-melt salt component, same units and sign as salt_flux (positive salinifies; &ocean_cavity_melt_nml). OWNED by rdb_ocean_cavity_flux and never written by the ice coupler, which full-overwrites salt_flux.

The fixed-mass dilution equivalent.

salt_cavity = -m_mass*(S_far - s_ice)

is the exact fixed-mass equivalent of adding mass m_mass at salinity s_ice — see the derivation in rdb_ocean_cavity_flux’s module docstring. Melting (m_mass > 0, S_far > s_ice) therefore FRESHENS.

Under &ocean_cavity_melt_nml freshwater="virtual" (the default) that IS the meltwater’s whole effect: no mass moves.

Under freshwater="mass" the meltwater is a REAL volume source on the top layer and this component is NOT the salinity tendency any more — but it is STILL assembled into Q_salt unchanged, because Q_salt is also what KPP and EPBL read to build B_0, and this term is the dominant (freshening) part of the surface buoyancy flux there. ocean_cavity_mass_step takes the increment back out of the SALINITY TRACER (and out of the pseudo-salt mirror) in the same stage, as the exact negation of what apply_surface_src_2d_impl stamped. So: one field, two readers, and only the tracer reader is corrected.

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

Net surface salt-flux COMPONENT (kg salt/m^2/s, positive salinifies) — a filler writes this (e.g. the ice brine coupler); the assembler adds Q_salt_const to produce Q_salt. Virtual in v1 (no column-mass change).

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

Sea-ice melt-water mass flux (kg/m^2/s, >0 = melt into the ocean, <0 = formation / freezing withdraws mass).

real(kind=wp), public :: sw_band_ratio = 0.58_wp

Band-1 weight R of the two-band irradiance decay. Jerlov type I (clear open ocean) default.

logical, public :: sw_from_qsw = .false.

Selects the irradiance source for shortwave penetration and the boundary-layer SW coupling. .false. (default): the source is the NET heat flux Q_heat (sw_source="net_heat", legacy, bit-identical). .true. (sw_source="q_sw"): the source is the dedicated q_sw component (>= 0), which removes the night-time negative-I0 hazard where sw_pen_frac*Q_heat < 0 drives unphysical negative irradiance down the two-band profile. Set by set_sw_penetration; host-side gate only (never a device reduction) — the shim selects the source array on the host so the conditionally-allocated q_sw is never dereferenced in a device kernel.

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

Penetrating fraction of Q_heat carried below the surface layer as a two-band exponential (Paulson & Simpson 1977). 0 = off (all of Q_heat lands at k = nz, legacy path).

real(kind=wp), public :: sw_zeta1 = 0.35_wp

Band-1 e-folding depth (m) — the rapidly-absorbed red/near-IR band.

real(kind=wp), public :: sw_zeta2 = 23.0_wp

Band-2 e-folding depth (m) — the slowly-absorbed blue/green band.

logical, public :: use_components = .false.

Master gate (&ocean_forcing_nml enable_components). .false. (default): none of the arrays below are allocated, ocean_surface_flux_assemble is a no-op, and Q_heat/Q_salt are filled exactly as today — bit-identical. .true.: allocates the component set (set_components) and the assembler rebuilds Q_heat/Q_salt every thermo step.

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

Virtual precipitation (kg/m^2/s, either sign — SSS-restoring convention; NOT wired to the restoring kernel in v1, see set_restore below and the module docstring’s follow-up note).


Type-Bound Procedures

procedure, public, non_overridable :: bytes => ocean_surface_flux_bytes

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

    Counted allocatable footprint of the surface flux slot (0 when unallocated). One arr_bytes term per array — add a term here when a new allocatable joins the type.

    Arguments

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

    Return Value integer(kind=int64)

procedure, public, non_overridable :: destroy => ocean_surfflux_destroy

procedure, public, non_overridable :: enter_data => ocean_surfflux_enter_data

  • private subroutine ocean_surfflux_enter_data(this)

    Type-bound wrapper — delegates to the non-polymorphic impl so the device-attach map base is the heap object, not a polymorphic stack box (AMD libomptarget cross-slot-overlap fix).

    Arguments

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

procedure, public, non_overridable :: exit_data => ocean_surfflux_exit_data

procedure, public, non_overridable :: init => ocean_surfflux_init

procedure, public, non_overridable :: set_components => ocean_surfflux_set_components

  • private subroutine ocean_surfflux_set_components(this, grid, enable)

    Configure-time gate for the PR-12 component set (&ocean_forcing_nml enable_components). init runs before the namelist gate is known, so allocation happens HERE rather than in init: enable = .false. (default) leaves use_components false and allocates nothing — bit-identical, zero extra device memory. enable = .true. allocates the full component set (source=0.0_wp) and flips the gate so ocean_surface_flux_assemble stops early-returning. Must be called BEFORE enter_data (rdb_ocean_state.F90’s orchestrator) so the freshly-allocated arrays get mapped. Re-entrant: calling again with a different enable deallocates first.

    Arguments

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

procedure, public, non_overridable :: set_p_surf_const => ocean_surfflux_set_p_surf_const

  • private subroutine ocean_surfflux_set_p_surf_const(this, p_surf_val)

    Seed the atmospheric surface-pressure INPUT component p_surf_atm (Pa) uniformly from a scalar namelist value (PR-17 &ocean_psurf_nml p_surf_const). Full overwrite of the pristine atmospheric base; the assembled total p_surf is built from it once per outer step in p_surf_update_seam. No-op when the component set is not allocated (use_components=.false.) — the &ocean_psurf_nml enable guard in validate_config already requires enable_components=.true., so a live consumer never hits the no-op. Host only — call enter_data afterwards (or !$acc update device if already mapped) to sync to the GPU.

    Read more…

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_surface_flux_t), intent(inout) :: this
    real(kind=wp), intent(in) :: p_surf_val

procedure, public, non_overridable :: set_restore => ocean_surfflux_set_restore

  • private subroutine ocean_surfflux_set_restore(this, enable_T, enable_S, piston_t_day, piston_s_day, T_target, S_target)

    Seed the surface buoyancy restoring (MOM6 RESTOREBUOY) parameters and set the has_restore_T / has_restore_S gates. Sibling to set_surface_flux_const / set_sw_penetration — kept separate so existing callers are unchanged. The piston velocities arrive in m/day (the MOM6 FLUXCONST_* unit) and are converted to MKS m/s here. Effective-enable guard: has_restore_* = enable_* .and. piston /= 0, so an enabled switch with a zero piston is a silent no-op (rather than restoring everything toward the 0-degC / 0-PSU default target). Host only; all knobs are read host-side as by-value arguments to the device _impl, so no extra device sync beyond copyin(this).

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_surface_flux_t), intent(inout) :: this
    logical, intent(in) :: enable_T
    logical, intent(in) :: enable_S
    real(kind=wp), intent(in) :: piston_t_day
    real(kind=wp), intent(in) :: piston_s_day
    real(kind=wp), intent(in) :: T_target
    real(kind=wp), intent(in) :: S_target

procedure, public, non_overridable :: set_surface_flux_const => ocean_surfflux_set_const

  • private subroutine ocean_surfflux_set_const(this, q_heat_val, q_salt_val)

    Fill Q_heat / Q_salt uniformly from scalar values and set the has_heat / has_salt flags so the apply-tracers kernel fires. Mirrors set_wind_stress_const on the stress side. Host only — call enter_data afterwards (or !$acc update device if already mapped) to sync to the GPU.

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_surface_flux_t), intent(inout) :: this
    real(kind=wp), intent(in) :: q_heat_val
    real(kind=wp), intent(in) :: q_salt_val

procedure, public, non_overridable :: set_sw_penetration => ocean_surfflux_set_sw

  • private subroutine ocean_surfflux_set_sw(this, sw_pen_frac, sw_band_ratio, sw_zeta1, sw_zeta2, sw_source)

    Seed the shortwave-penetration band parameters and set the has_sw gate (sw_pen_frac /= 0). Sibling to set_surface_flux_const — kept separate so existing callers of the heat/salt setter are unchanged. Host only; the scalars are read host-side by the apply kernel (they parameterise the by-value arguments passed into the device _impl), so no extra device sync is needed beyond the existing copyin(this).

    Read more…

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_surface_flux_t), intent(inout) :: this
    real(kind=wp), intent(in) :: sw_pen_frac
    real(kind=wp), intent(in) :: sw_band_ratio
    real(kind=wp), intent(in) :: sw_zeta1
    real(kind=wp), intent(in) :: sw_zeta2
    character(len=*), intent(in), optional :: sw_source

Source Code

   type :: ocean_surface_flux_t
      logical :: is_init = .false.
         !! True between `init` and `destroy`.
      logical :: has_heat = .false.
         !! True when `Q_heat` carries a non-zero fill (set by
         !! `set_surface_flux_const` when `q_heat_val /= 0`).
         !! Any future field-fill path (A3 data-override, A4 restoring)
         !! MUST set `has_heat = .true.` after writing into `Q_heat`
         !! so the apply-tracers kernel fires.  Host-side flag only
         !! (early-return guard in `ocean_surface_flux_apply_tracers`).
      logical :: has_salt = .false.
         !! True when `Q_salt` carries a non-zero fill (set by
         !! `set_surface_flux_const` when `q_salt_val /= 0`).
         !! Same contract as `has_heat` for any field-fill path.
      real(wp) :: rho0 = 1035.0_wp
         !! Boussinesq reference density (kg/m^3) — the `dt/(rho0*cp)` heat
         !! and `dt/rho0` salt divisors applied to EVERY surface tracer
         !! source, including whatever the sea-ice coupler writes into
         !! `Q_heat`/`Q_salt`.
         !!
         !! ASSIGNED FROM CONFIG by `configure_ocean_reference_density`,
         !! which copies the single rho0 of record (`&ocean_ic_nml rho_0`
         !! -> `eos%rho0`).  The literal here is only the pre-configure
         !! type default; do not read it as the value a run uses.  Host
         !! scalar: the divisor is folded into the `inv_scale` argument on
         !! the host, so the assignment owes no `!$acc update device`.
      real(wp) :: cp = SEAWATER_CP
         !! Specific heat capacity (J/kg/K).
      real(wp) :: h_min = 1.0e-3_wp
         !! Floor on the surface-layer thickness in the `1/h_top`
         !! division — keeps the kernel finite when the top layer
         !! pinches out.
      real(wp) :: Q_heat_const = 0.0_wp
         !! Scalar fill source for `Q_heat(:,:)`.  Seeded from
         !! `&ocean_thermo_nml q_heat` by `set_surface_flux_const`.
         !! Kept for diagnostic logging; kernels read `Q_heat` directly.
      real(wp) :: Q_salt_const = 0.0_wp
         !! Scalar fill source for `Q_salt(:,:)`.  Seeded from
         !! `&ocean_thermo_nml q_salt` by `set_surface_flux_const`.

      logical :: has_sw = .false.
         !! True when shortwave penetration is active (set by
         !! `set_sw_penetration` when `sw_pen_frac /= 0`).  Host-side
         !! gate only — `ocean_surface_flux_apply_sw_penetration`
         !! early-returns unless this is set, so the default-off path
         !! leaves the surface-flux deposition byte-for-byte unchanged.
      logical :: sw_from_qsw = .false.
         !! Selects the irradiance source for shortwave penetration and
         !! the boundary-layer SW coupling.  `.false.` (default): the
         !! source is the NET heat flux `Q_heat` (`sw_source="net_heat"`,
         !! legacy, bit-identical).  `.true.` (`sw_source="q_sw"`): the
         !! source is the dedicated `q_sw` component (>= 0), which
         !! removes the night-time negative-`I0` hazard where
         !! `sw_pen_frac*Q_heat < 0` drives unphysical negative
         !! irradiance down the two-band profile.  Set by
         !! `set_sw_penetration`; host-side gate only (never a device
         !! reduction) — the shim selects the source array on the host so
         !! the conditionally-allocated `q_sw` is never dereferenced in a
         !! device kernel.
      real(wp) :: sw_pen_frac = 0.0_wp
         !! Penetrating fraction of `Q_heat` carried below the surface
         !! layer as a two-band exponential (Paulson & Simpson 1977).
         !! 0 = off (all of `Q_heat` lands at `k = nz`, legacy path).
      real(wp) :: sw_band_ratio = 0.58_wp
         !! Band-1 weight `R` of the two-band irradiance decay.  Jerlov
         !! type I (clear open ocean) default.
      real(wp) :: sw_zeta1 = 0.35_wp
         !! Band-1 e-folding depth (m) — the rapidly-absorbed
         !! red/near-IR band.
      real(wp) :: sw_zeta2 = 23.0_wp
         !! Band-2 e-folding depth (m) — the slowly-absorbed
         !! blue/green band.
      logical :: has_restore_T = .false.
         !! True when SST restoring is active (`enable_restore_temp .and.
         !! restore_piston_T /= 0`).  Host-side gate only —
         !! `ocean_surface_restore_apply_tracers` early-returns unless
         !! this or `has_restore_S` is set, so the default-off path is
         !! byte-for-byte unchanged.
      logical :: has_restore_S = .false.
         !! True when SSS restoring is active (`enable_restore_salt .and.
         !! restore_piston_S /= 0`).
      real(wp) :: restore_piston_T = 0.0_wp
         !! SST piston velocity (m/s), seeded from `&ocean_restore_nml
         !! piston_t` (m/day) via `/86400`.  The surface relaxation rate
         !! for a top layer of thickness `h_top` is
         !! `lambda = restore_piston_T / h_top` [1/s].
      real(wp) :: restore_piston_S = 0.0_wp
         !! SSS piston velocity (m/s).
      real(wp) :: restore_T_target = 0.0_wp
         !! Scalar target SST (degC).  Read by-value into the device
         !! `_impl` kernel — no per-cell field (so no extra device
         !! array, the `enter_data` orchestrator is untouched).  A
         !! 2D-field target is the documented A4-v2 follow-up.
      real(wp) :: restore_S_target = 0.0_wp
         !! Scalar target SSS (PSU).
      real(wp), allocatable :: Q_heat(:, :)
         !! 2D net surface heat flux (W/m^2, positive downward),
         !! shape `(nx, ny)`.  Fill via `set_surface_flux_const` for
         !! spatially-uniform forcing (the default); Area-A3 override
         !! or Area-A4 restoring writes the field directly.
      real(wp), allocatable :: Q_salt(:, :)
         !! 2D net surface salt flux (kg salt/m^2/s, positive salinifies),
         !! shape `(nx, ny)`.

      ! ---- PR-12 component set — allocated iff `use_components` ----
      logical :: use_components = .false.
         !! Master gate (`&ocean_forcing_nml enable_components`).  `.false.`
         !! (default): none of the arrays below are allocated,
         !! `ocean_surface_flux_assemble` is a no-op, and `Q_heat`/`Q_salt`
         !! are filled exactly as today — bit-identical.  `.true.`:
         !! allocates the component set (`set_components`) and the
         !! assembler rebuilds `Q_heat`/`Q_salt` every thermo step.
      real(wp) :: q_assembled = 0.0_wp
         !! Host latch, 1 once `ocean_surface_flux_assemble` has derived
         !! `Q_heat`/`Q_salt` from the component set.  From then on the
         !! two arrays are CARRIED state: the assembler runs at the END of
         !! a thermo step and the steps up to the next one read what it
         !! left (SST-dependent terms included), so with `use_components`
         !! they are checkpointed, and this latch tells a warm restart
         !! that the checkpointed arrays are an assembly to resume from
         !! (0 => an older checkpoint or none yet: the configure-time
         !! seed stands).  A real, not a logical, because the restart
         !! registry carries real scalars.
      logical :: has_mass_flux = .false.
         !! Host-side latch — set by a filler after writing ANY of
         !! `evap`/`lprec`/`fprec`/`vprec`/`lrunoff`/`frunoff`/
         !! `seaice_melt`.  Cheap early-return gate for a future
         !! freshwater kernel (real-mass PR).  Same contract as
         !! `has_heat` (`:56-62`) — never set from a device reduction.
      logical :: has_q_sw = .false.
         !! Host-side latch — set by a filler after writing `q_sw`
         !! (e.g. an ice `sw_thru` coupler).  **Not** the same as
         !! `has_sw` below (that gates shortwave *penetration*, an
         !! unrelated pre-existing switch) — do not conflate the two.

      real(wp), allocatable :: q_sw(:, :)
         !! Shortwave into the ocean (W/m^2, **>= 0**, positive down).
      real(wp), allocatable :: q_lw(:, :)
         !! Net longwave (W/m^2, typically **< 0**, positive down).
      real(wp), allocatable :: q_lat(:, :)
         !! Latent heat flux (W/m^2, typically **< 0**, positive down).
      real(wp), allocatable :: q_sens(:, :)
         !! Sensible heat flux (W/m^2, typically **< 0**, positive down).
      real(wp), allocatable :: heat_added(:, :)
         !! Restoring / flux-adjustment / "other" net heat term not
         !! decomposed into the radiative/turbulent bands above (W/m^2,
         !! either sign; MOM6 `heat_added`).  The v1 ice coupler's
         !! `heat_flux_diag` lands here (§5.4 of the PR-12 plan) —
         !! it is already a net W/m^2, not further decomposable.
      real(wp), allocatable :: heat_cavity(:, :)
         !! **Ice-shelf cavity basal-melt heat component** (W/m^2, same
         !! positive-DOWN-into-the-ocean convention as every other heat
         !! band; `&ocean_cavity_melt_nml`).  OWNED by
         !! `rdb_ocean_cavity_flux`; written `heat_cavity = -q_ocean`,
         !! where `q_ocean = rho_w*c_w*gamma_t*(T_w - T_b) > 0` is the
         !! kernel's turbulent heat flux OCEAN -> INTERFACE, so warm
         !! water under a shelf COOLS the top of the column.  It is a
         !! SEPARATE field from `heat_added` precisely because the
         !! sea-ice coupler full-overwrites `heat_added` — two writers
         !! on one slot clobber silently (cavity x sea ice is refused
         !! today, but the ownership rule must not depend on that).
         !! Zero unless a cavity melt step ran.

      real(wp), allocatable :: evap(:, :)
         !! Evaporative mass flux (kg/m^2/s, **<= 0** — MOM6 convention,
         !! `(-1)*flux out of the ocean`).  v1: enthalpy + salt
         !! bookkeeping only — does NOT change column mass (real
         !! freshwater is a named follow-up, see the module docstring).
      real(wp), allocatable :: lprec(:, :)
         !! Liquid precipitation (kg/m^2/s, **>= 0** into the ocean).
      real(wp), allocatable :: fprec(:, :)
         !! Frozen precipitation / snowfall (kg/m^2/s, **>= 0**).
      real(wp), allocatable :: vprec(:, :)
         !! Virtual precipitation (kg/m^2/s, either sign — SSS-restoring
         !! convention; NOT wired to the restoring kernel in v1, see
         !! `set_restore` below and the module docstring's follow-up note).
      real(wp), allocatable :: lrunoff(:, :)
         !! Liquid river runoff (kg/m^2/s, **>= 0**).
      real(wp), allocatable :: frunoff(:, :)
         !! Frozen (calving/ice) runoff (kg/m^2/s, **>= 0**).
      real(wp), allocatable :: seaice_melt(:, :)
         !! Sea-ice melt-water mass flux (kg/m^2/s, **>0** = melt into
         !! the ocean, **<0** = formation / freezing withdraws mass).

      real(wp), allocatable :: heat_content_lprec(:, :)
         !! Enthalpy carried by `lprec` (W/m^2).  A filler that writes
         !! `lprec` MUST fill this — the v1 convenience is
         !! `SEAWATER_CP * T_source * lprec`.  The source (not the
         !! ocean) owns this enthalpy — see the module docstring §(d).
      real(wp), allocatable :: heat_content_fprec(:, :)
         !! Enthalpy carried by `fprec` (W/m^2).
      real(wp), allocatable :: heat_content_vprec(:, :)
         !! Enthalpy carried by `vprec` (W/m^2).
      real(wp), allocatable :: heat_content_lrunoff(:, :)
         !! Enthalpy carried by `lrunoff` (W/m^2).
      real(wp), allocatable :: heat_content_frunoff(:, :)
         !! Enthalpy carried by `frunoff` (W/m^2).
      real(wp), allocatable :: heat_content_seaice_melt(:, :)
         !! Enthalpy carried by `seaice_melt` (W/m^2).
      real(wp), allocatable :: heat_content_massin(:, :)
         !! **Assembler output — do NOT write.**  Sum of the six
         !! `heat_content_<flux>` companions above (W/m^2, >= 0 for warm
         !! inflow).  Filled by `ocean_surface_flux_assemble`.
      real(wp), allocatable :: heat_content_massout(:, :)
         !! **Assembler output — do NOT write.**  `SEAWATER_CP * T_sst *
         !! evap` (W/m^2, <= 0 since `evap <= 0`) — the enthalpy the ocean
         !! loses with evaporating mass, computed from the ocean's own
         !! surface temperature (the ocean, not a filler, owns this
         !! number).  There is deliberately NO `heat_content_evap` field
         !! — see the PR-12 plan §11.6.  Filled by
         !! `ocean_surface_flux_assemble`.

      real(wp), allocatable :: salt_flux(:, :)
         !! Net surface salt-flux COMPONENT (kg salt/m^2/s, **positive
         !! salinifies**) — a filler writes this (e.g. the ice brine
         !! coupler); the assembler adds `Q_salt_const` to produce
         !! `Q_salt`.  Virtual in v1 (no column-mass change).
      real(wp), allocatable :: salt_cavity(:, :)
         !! **Ice-shelf cavity basal-melt salt component**, same units
         !! and sign as `salt_flux` (positive salinifies;
         !! `&ocean_cavity_melt_nml`).  OWNED by `rdb_ocean_cavity_flux`
         !! and never written by the ice coupler, which full-overwrites
         !! `salt_flux`.
         !!
         !! **The fixed-mass dilution equivalent.**
         !!
         !!   `salt_cavity = -m_mass*(S_far - s_ice)`
         !!
         !! is the exact fixed-mass equivalent of adding mass `m_mass` at
         !! salinity `s_ice` — see the derivation in
         !! `rdb_ocean_cavity_flux`'s module docstring.  Melting
         !! (`m_mass > 0`, `S_far > s_ice`) therefore FRESHENS.
         !!
         !! Under `&ocean_cavity_melt_nml freshwater="virtual"` (the
         !! default) that IS the meltwater's whole effect: no mass moves.
         !!
         !! Under `freshwater="mass"` the meltwater is a REAL volume
         !! source on the top layer and this component is NOT the
         !! salinity tendency any more — but it is STILL assembled into
         !! `Q_salt` unchanged, because `Q_salt` is also what KPP and
         !! EPBL read to build `B_0`, and this term is the dominant
         !! (freshening) part of the surface buoyancy flux there.
         !! `ocean_cavity_mass_step` takes the increment back out of the
         !! SALINITY TRACER (and out of the pseudo-salt mirror) in the
         !! same stage, as the exact negation of what
         !! `apply_surface_src_2d_impl` stamped.  So: one field, two
         !! readers, and only the tracer reader is corrected.

      real(wp), allocatable :: p_surf_atm(:, :)
         !! **Input component.**  Atmospheric surface-pressure load
         !! (Pa, >= 0).  Filled by an external reader / configure-time
         !! scalar seed — the sea-ice path never writes this field.
         !! Ships zeroed with no consumer in this PR (the inverse-
         !! barometer PGF fold is a same-release-cycle follow-up).
      real(wp), allocatable :: p_surf(:, :)
         !! **Assembled total** (Pa, >= 0) — `p_surf_atm` plus any ice
         !! mass-loading term, **full overwrite, never `+=`** (a `+=`
         !! ratchets the load across outer steps with no bound).  No
         !! consumer in this PR; ships zeroed alongside `p_surf_atm` so
         !! the follow-up PGF fold needs no further plumbing.
   contains
      procedure, non_overridable :: init => ocean_surfflux_init
      procedure, non_overridable :: destroy => ocean_surfflux_destroy
      procedure, non_overridable :: enter_data => ocean_surfflux_enter_data
      procedure, non_overridable :: exit_data => ocean_surfflux_exit_data
      procedure, non_overridable :: set_surface_flux_const => ocean_surfflux_set_const
      procedure, non_overridable :: set_sw_penetration => ocean_surfflux_set_sw
      procedure, non_overridable :: set_restore => ocean_surfflux_set_restore
      procedure, non_overridable :: set_components => ocean_surfflux_set_components
      procedure, non_overridable :: set_p_surf_const => ocean_surfflux_set_p_surf_const
      procedure, non_overridable :: bytes => ocean_surface_flux_bytes
   end type ocean_surface_flux_t