ocean_dyn_t Derived Type

type, public :: ocean_dyn_t


Inherits

type~~ocean_dyn_t~~InheritsGraph type~ocean_dyn_t ocean_dyn_t type~barotropic_workstate_t barotropic_workstate_t type~ocean_dyn_t->type~barotropic_workstate_t bt_work type~bt_wide_t bt_wide_t type~ocean_dyn_t->type~bt_wide_t bt_wide type~chksum_probe_t chksum_probe_t type~ocean_dyn_t->type~chksum_probe_t chksum_probe type~ke_probe_t ke_probe_t type~ocean_dyn_t->type~ke_probe_t ke_probe type~local_bt_cont_u_type local_BT_cont_u_type type~barotropic_workstate_t->type~local_bt_cont_u_type BTCL_u type~local_bt_cont_v_type local_BT_cont_v_type type~barotropic_workstate_t->type~local_bt_cont_v_type BTCL_v type~hgrid_t hgrid_t type~bt_wide_t->type~hgrid_t grid_w type~ocean_metrics_t ocean_metrics_t type~bt_wide_t->type~ocean_metrics_t metrics_w

Inherited by

type~~ocean_dyn_t~~InheritedByGraph type~ocean_dyn_t ocean_dyn_t type~ocean_state_t ocean_state_t type~ocean_state_t->type~ocean_dyn_t dyn 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
logical, public :: accel_visc_rem = .false.

MOM6 MOM_dynamics_split_RK2 parity: attenuate the slow EXPLICIT accelerations by the per-layer viscous remnant — u_new = u_entry + visc_rem·(u_applied − u_entry) after the CorAdv/PGF/hvisc/drag applies, before the BT correction — so friction-dominated near-massless layers cannot receive a full-strength dt·F kick (the 2026-07-28 forensics: explicit force × dt on outcropped mm-layers is the dt=800 blow-up injector; the fold/vdiff mopped ±30 m/s per stage until escape). &ocean_vdiff_nml accel_visc_rem — RETIRED (D1 follow-up): no MOM6 state-update equivalent, fail-loud at configure. Default off ⇒ bit-identical. Split path only (v1).

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

Phase-2/3 copy of cfg%ocean%isopycnal%angstrom_h (m). Used to compute isopycnal_vanish_tol for the reset and CFL gates; 0 ⇒ off ⇒ bit-identical.

real(kind=wp), public, allocatable :: avr_u0(:,:,:)
real(kind=wp), public, allocatable :: avr_v0(:,:,:)

accel_visc_rem stage-entry velocity snapshots. Eagerly allocated at setup (configure_ocean_bt) and device-mapped in ocean_dyn_enter_data_impl — but ONLY when the accel_visc_rem knob is on (unallocated / zero-footprint on the default-off path); released in destroy/exit_data.

integer, public :: bt_halo = 0

Wide-halo march-in width (0 = per-substep v1, bit-identical). Set by ocean_dyn_enable_bt_wide after init and enter_data.

integer, public :: bt_nonfin_step = 0

Faces whose BT-correction Δ was non-finite this stage (fold write skipped; pdc 2d5338f9). 0 on a healthy run.

type(bt_wide_t), public, allocatable :: bt_wide

Wide shadow state; allocated only when bt_halo > 0.

type(barotropic_workstate_t), public :: bt_work
logical, public :: cfl_ignore_vanished = .false.

Phase 3: when .true. AND vcoord is VCOORD_LAGRANGIAN, pass vanish_tol to compute_max_cfl and apply_velocity_truncation so vanished-layer face spikes are excluded from MaxCFL / panic and are zeroed (not CFL-clipped) by truncation.

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

Max realised inner CFL within the last outer step.

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

Realised outer CFL (diagnostic).

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

Advective-CFL truncation threshold (nondim). When > 0, any face whose |u|·dt/dx exceeds it is clipped to 0.9·cfl_trunc·dx/dt (sign preserved), then the maxvel cap is applied. 0 = off (bit-identical).

logical, public :: check_h_positive = .false.

DEBUG copy of cfg%ocean%isopycnal%check_h_positive. When .true., check_h_positive_or_die runs after each h_layer- writing stage and aborts on the first negative thickness, naming the stage. .false. ⇒ never called ⇒ bit-identical.

type(chksum_probe_t), public :: chksum_probe

MOM6-style per-phase field checksums + HOTFACE argmax rows (&ocean_debug_nml chksum + step window). Default off ⇒ bit-identical; when on, waits the device + reduces at each phase seam — the first-diverging-operator attribution probe.

logical, public :: debug_bt_budget = .false.

When .true., calls the BT-budget probe once per RK2 stage after the slow tendencies. Heavy (D->H transfers + prints). Default .false.; driven by ocean_debug_bt_budget namelist.

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

Inner barotropic substep length (s).

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

Outer baroclinic timestep length (s).

integer, public :: dt_therm_ratio = 1

Tracer/thermo step runs every dt_therm_ratio outer steps with effective dt = ratio · dt. 1 (default) = every step (bit-identical). Applies only when enable_thermodynamics.

integer, public :: dt_tracer_advect_ratio = 1

MOM6 DT_TRACER_ADVECT analogue. Horizontal tracer advection fires every dt_tracer_advect_ratio outer steps over the accumulated face transports (continuity_t%uhtr/vhtr). 1 (default) ⇒ every-step path verbatim (bit-identical). Configure requires dt_therm_ratio to be an integer multiple so the ALE remap never fires mid-accumulation-window.

logical, public :: enable_thermodynamics = .true.

Mirrors MOM6 ENABLE_THERMODYNAMICS. When .false. the step skips EOS, tracer hdiff, tracer vertical advection/diffusion, KPP non-local transport, and surface tracer flux. Tracers still advect horizontally but with alpha_T = beta_S = 0 can’t influence dynamics ⇒ adiabatic. Wire via &ocean_setup_nml ocean_enable_thermodynamics.

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

Exponential growth rate of the surface value (1/s), set by configure_ocean_tracers from &ocean_tracers_nml ideal_age_sfc_growth_rate. 0 (default) ⇒ young_val constant ⇒ no exp() ⇒ bit-identical.

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

Surface-band age value (s), set by configure_ocean_tracers from &ocean_tracers_nml ideal_age_young_val. 0 (default) = today’s hard-coded zero reset ⇒ bit-identical.

logical, public :: is_init = .false.

True between init and destroy. Prefer this to allocated(...) — tracks GPU device attachment too.

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

Total kinetic energy after the last outer step (diagnostic).

type(ke_probe_t), public :: ke_probe

Per-segment layer-KE attribution meter (&ocean_debug_nml ke_attr). Default off ⇒ bit-identical; when on, serialises the async velocity-apply chain (device waits + one reduction per segment) — debug only.

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

When > 0, face velocities are clipped to [-maxvel, +maxvel] after each outer step (safety net, not a fix). 0 = off.

integer, public :: n_inner = 0

Number of barotropic substeps per outer baroclinic step (0 = “auto, derived from CFL”).

integer, public :: n_nanzero_step = 0

Non-finite (NaN/Inf) face velocities zeroed by the truncation’s NaN-catch in the most recent outer step. MUST be 0 on a healthy run; any non-zero is a producer 0/0 upstream (loud — NaNs used to launder to ±maxvel silently, pdc 8c2fd674).

integer(kind=int64), public :: n_nanzero_total = 0_int64

Cumulative NaN-catch count over the run.

integer, public :: ntrunc_step = 0

Face components CFL-truncated in the most recent outer step (host-scalar reduction; reset each apply_velocity_truncation).

integer, public :: ntrunc_total = 0

Cumulative CFL-truncation count over the run (driver logs it).

integer, public :: outer_step_count = 0

Outer (baroclinic) step counter.

real(kind=wp), public :: pc_be = 0.6_wp

pred_corr predictor fraction (MOM6 BE, 0.6 in the control run): the predictor advances the provisional velocity to dt_pred = pc_be·dt (SPEC §2 P8); only the corrector takes the full step. Unused under ssp_rk2.

logical, public :: poison_ghosts = .false.

When .true., sentinel-NaN the exchange-covered ghost bands at each outer-step start (before any exchange or kernel). Any kernel consuming an unexchanged ghost produces a loud NaN at that step. Default .false. = bit-identical (untaken branch per step). Driven by &ocean_mpi_nml poison_ghosts via rdb_ocean_setup. Requires the bc optional argument (edge topology); no-op when bc is absent.

logical, public :: reset_vanished_u = .false.

Phase 2: when .true. AND vcoord is VCOORD_LAGRANGIAN, call reset_vanished_layer_velocities after each mask_layer_velocities (per-stage + RK2-averaged site) to zero face velocities where BOTH adjacent cell-thicknesses are at or below isopycnal_vanish_tol(angstrom_h).

integer, public :: split_scheme = SPLIT_SCHEME_PRED_CORR

Outer time-scheme selector (see the module constants). Kept in step with the &ocean_bt_nml split_scheme default in rdb_config.F90 — ocean_setup assigns this field from the config on BOTH branches (rdb_ocean_setup.F90, “SPEC S3”), so no production run reads this value, but a bare ocean_dyn_t built by a unit test inherits it and a divergence would have the Fortran suite exercising a scheme the model does not ship by default. The two have diverged before; change them in the same commit.

Why the default moved (2026-09-14). ssp_rk2 grows internal gravity waves out of a stratified REST state — En 2.992E-05 against pred_corr’s 1.739E-09 at day 25 on resting_stratified_channel.nml, 17 000× — and the three things that kept the predictor-corrector off the default are all closed: the land-mask NaN (the fast-loop Coriolis reference was evaluated on the stage-entry u^n while the slow Coriolis used u_av; see set_cor_ref_velocity), the nz = 1 vdiff tridiagonal defect, and SEVEN GPU-only unit-test failures — periodic, obc_baroclinic, dyn_split, ice_restart, wetdry (non-finite within 1-5 steps), plus restart bit-exactness and uniform-p_surf gauge invariance — which had ONE cause, and it was not scheme semantics: scratch_3d_buffer_t attached its payload with !$acc enter data create, which carries no host value, and the pred_corr PREDICTOR deliberately reads hv%du_visc/dv_visc without recomputing them (MOM6’s diffu(u[n-1]) reuse). At step 1 there is no previous producer, so the host read the zero init promised and the device read the allocator’s leftovers. enter_data now device-zeroes the payload (rdb_test_scratch_3d_device is the gate), and ctest -R rdb is 187/187 on BOTH toolchains.


Type-Bound Procedures

procedure, public, non_overridable :: bytes => ocean_dyn_bytes

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

    Counted allocatable footprint of the split-RK2 driver (BT work state + wide-halo shadow state) slot (0 when unallocated).

    Arguments

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

    Return Value integer(kind=int64)

procedure, public, non_overridable :: destroy => ocean_dyn_destroy

procedure, public, non_overridable :: enter_data => ocean_dyn_enter_data

procedure, public, non_overridable :: exit_data => ocean_dyn_exit_data

procedure, public, non_overridable :: init => ocean_dyn_init

  • private subroutine ocean_dyn_init(this, grid, nz_ml)

    Initialise the barotropic working-state slot. Pass nz_ml to also allocate the split-driver slow-tendency accumulators (F_slow_u/v, F_bt_u/v, ubt_at_n/vbt_at_n). Barotropic-substep unit tests can skip the optional argument since they don’t exercise the split-driver coupling.

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_dyn_t), intent(inout) :: this
    type(hgrid_t), intent(in) :: grid
    integer, intent(in), optional :: nz_ml

procedure, public, non_overridable :: is_thermo_step => ocean_dyn_is_thermo_step

  • private pure function ocean_dyn_is_thermo_step(this) result(yes)

    Returns .true. if the thermodynamic / tracer kernels should fire on this outer step. dt_therm_ratio <= 1 (default) → always true (every step is a thermo step, bit-identical to prior behaviour). ratio >= 2 → fires every Nth step, aligned to outer_step_count = 0 for the IC snapshot.

    Arguments

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

    Return Value logical

procedure, public, non_overridable :: is_tracer_advect_step => ocean_dyn_is_tracer_advect_step

  • private pure function ocean_dyn_is_tracer_advect_step(this) result(yes)

    Returns .true. when the windowed horizontal tracer-advect drain should fire on the current outer step, evaluated with the PRE-increment outer_step_count so it aligns bit-for-bit with is_thermo_step() (which gates the ALE remap on the same count).

    Read more…

    Arguments

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

    Return Value logical

procedure, public, non_overridable :: therm_dt => ocean_dyn_therm_dt

  • private pure function ocean_dyn_therm_dt(this, dt) result(dt_th)

    Effective dt for the thermo / tracer kernels. When dt_therm_ratio = 1 returns dt exactly; otherwise returns ratio · dt, since the kernels only fire every Nth step and must advance by that aggregate interval.

    Arguments

    Type IntentOptional Attributes Name
    class(ocean_dyn_t), intent(in) :: this
    real(kind=wp), intent(in) :: dt

    Return Value real(kind=wp)

Source Code

   type :: ocean_dyn_t
      logical :: is_init = .false.
         !! True between `init` and `destroy`.  Prefer this to
         !! `allocated(...)` — tracks GPU device attachment too.
      integer :: split_scheme = SPLIT_SCHEME_PRED_CORR
         !! Outer time-scheme selector (see the module constants).  Kept in
         !! step with the `&ocean_bt_nml split_scheme` default in
         !! `rdb_config.F90` — `ocean_setup` assigns this field from the
         !! config on BOTH branches (`rdb_ocean_setup.F90`, "SPEC S3"), so no
         !! production run reads this value, but a bare `ocean_dyn_t` built by
         !! a unit test inherits it and a divergence would have the Fortran
         !! suite exercising a scheme the model does not ship by default.
         !! The two have diverged before; change them in the same commit.
         !!
         !! **Why the default moved (2026-09-14).**  `ssp_rk2` grows
         !! internal gravity waves out of a stratified REST state — En
         !! 2.992E-05 against pred_corr's 1.739E-09 at day 25 on
         !! `resting_stratified_channel.nml`, 17 000× — and the three
         !! things that kept the predictor-corrector off the default are
         !! all closed: the land-mask NaN (the fast-loop Coriolis
         !! reference was evaluated on the stage-entry `u^n` while the
         !! slow Coriolis used `u_av`; see `set_cor_ref_velocity`), the
         !! `nz = 1` vdiff tridiagonal defect, and SEVEN GPU-only
         !! unit-test failures — `periodic`, `obc_baroclinic`,
         !! `dyn_split`, `ice_restart`, `wetdry` (non-finite within 1-5
         !! steps), plus `restart` bit-exactness and uniform-`p_surf`
         !! gauge invariance — which had ONE cause, and it was not scheme
         !! semantics: `scratch_3d_buffer_t` attached its payload with
         !! `!$acc enter data create`, which carries no host value, and
         !! the pred_corr PREDICTOR deliberately reads
         !! `hv%du_visc`/`dv_visc` without recomputing them (MOM6's
         !! `diffu(u[n-1])` reuse).  At step 1 there is no previous
         !! producer, so the host read the zero `init` promised and the
         !! device read the allocator's leftovers.  `enter_data` now
         !! device-zeroes the payload (`rdb_test_scratch_3d_device` is the
         !! gate), and `ctest -R rdb` is 187/187 on BOTH toolchains.
      real(wp) :: pc_be = 0.6_wp
         !! pred_corr predictor fraction (MOM6 `BE`, 0.6 in the control
         !! run): the predictor advances the provisional velocity to
         !! `dt_pred = pc_be·dt` (SPEC §2 P8); only the corrector takes
         !! the full step.  Unused under ssp_rk2.
      integer :: n_inner = 0
         !! Number of barotropic substeps per outer baroclinic step
         !! (0 = "auto, derived from CFL").
      integer :: outer_step_count = 0
         !! Outer (baroclinic) step counter.
      real(wp) :: dt_inner = 0.0_wp
         !! Inner barotropic substep length (s).
      real(wp) :: dt_outer = 0.0_wp
         !! Outer baroclinic timestep length (s).

      ! ---- Barotropic working state ----
      ! 2D barotropic-substep fields, time-mean accumulators, nonlinear
      ! scratch, and slow-tendency accumulators.  The substep kernels
      ! (`rdb_barotropic_substep`) + coupling kernels
      ! (`rdb_barotropic_coupling`) operate on `dyn%bt_work` directly.
      type(barotropic_workstate_t) :: bt_work

      ! ---- Diagnostic accumulators ----
      real(wp) :: ke_outer = 0.0_wp
         !! Total kinetic energy after the last outer step (diagnostic).
      real(wp) :: cfl_outer = 0.0_wp
         !! Realised outer CFL (diagnostic).
      real(wp) :: cfl_inner_max = 0.0_wp
         !! Max realised inner CFL within the last outer step.

      ! ---- Velocity-truncation clamp (MOM6 MAXVEL) ----
      real(wp) :: maxvel = 0.0_wp
         !! When > 0, face velocities are clipped to `[-maxvel, +maxvel]`
         !! after each outer step (safety net, not a fix).  0 = off.
      real(wp) :: cfl_trunc = 0.0_wp
         !! Advective-CFL truncation threshold (nondim).  When > 0, any
         !! face whose `|u|·dt/dx` exceeds it is clipped to
         !! `0.9·cfl_trunc·dx/dt` (sign preserved), then the `maxvel`
         !! cap is applied.  0 = off (bit-identical).
      integer :: ntrunc_step = 0
         !! Face components CFL-truncated in the most recent outer step
         !! (host-scalar reduction; reset each `apply_velocity_truncation`).
      integer :: ntrunc_total = 0
         !! Cumulative CFL-truncation count over the run (driver logs it).
      integer :: n_nanzero_step = 0
         !! Non-finite (NaN/Inf) face velocities zeroed by the truncation's
         !! NaN-catch in the most recent outer step.  MUST be 0 on a
         !! healthy run; any non-zero is a producer 0/0 upstream (loud —
         !! NaNs used to launder to ±maxvel silently, pdc 8c2fd674).
      integer(int64) :: n_nanzero_total = 0_int64
         !! Cumulative NaN-catch count over the run.
      integer :: bt_nonfin_step = 0
         !! Faces whose BT-correction Δ was non-finite this stage (fold
         !! write skipped; pdc 2d5338f9).  0 on a healthy run.

      ! ---- DT_THERM ratio (MOM6) ----
      integer :: dt_therm_ratio = 1
         !! Tracer/thermo step runs every `dt_therm_ratio` outer steps
         !! with effective dt = ratio · dt.  1 (default) = every step
         !! (bit-identical).  Applies only when `enable_thermodynamics`.
      integer :: dt_tracer_advect_ratio = 1
         !! MOM6 DT_TRACER_ADVECT analogue.  Horizontal tracer advection
         !! fires every `dt_tracer_advect_ratio` outer steps over the
         !! accumulated face transports (`continuity_t%uhtr/vhtr`).
         !! 1 (default) ⇒ every-step path verbatim (bit-identical).
         !! Configure requires `dt_therm_ratio` to be an integer multiple
         !! so the ALE remap never fires mid-accumulation-window.

      ! ---- Debug probe ----
      logical :: debug_bt_budget = .false.
         !! When `.true.`, calls the BT-budget probe once per RK2 stage
         !! after the slow tendencies.  Heavy (D->H transfers + prints).
         !! Default `.false.`; driven by `ocean_debug_bt_budget` namelist.
      type(ke_probe_t) :: ke_probe
         !! Per-segment layer-KE attribution meter (`&ocean_debug_nml
         !! ke_attr`).  Default off ⇒ bit-identical; when on,
         !! serialises the async velocity-apply chain (device waits +
         !! one reduction per segment) — debug only.
      type(chksum_probe_t) :: chksum_probe
         !! MOM6-style per-phase field checksums + HOTFACE argmax rows
         !! (`&ocean_debug_nml chksum` + step window).  Default off ⇒
         !! bit-identical; when on, waits the device + reduces at each
         !! phase seam — the first-diverging-operator attribution probe.
      logical :: accel_visc_rem = .false.
         !! MOM6 `MOM_dynamics_split_RK2` parity: attenuate the slow
         !! EXPLICIT accelerations by the per-layer viscous remnant —
         !! `u_new = u_entry + visc_rem·(u_applied − u_entry)` after the
         !! CorAdv/PGF/hvisc/drag applies, before the BT correction —
         !! so friction-dominated near-massless layers cannot receive a
         !! full-strength dt·F kick (the 2026-07-28 forensics: explicit
         !! force × dt on outcropped mm-layers is the dt=800 blow-up
         !! injector; the fold/vdiff mopped ±30 m/s per stage until
         !! escape).  `&ocean_vdiff_nml accel_visc_rem` — RETIRED (D1
         !! follow-up): no MOM6 state-update equivalent, fail-loud at
         !! configure.  Default off ⇒ bit-identical.  Split path only (v1).
      real(wp), allocatable :: avr_u0(:, :, :)
      real(wp), allocatable :: avr_v0(:, :, :)
         !! accel_visc_rem stage-entry velocity snapshots.  Eagerly
         !! allocated at setup (`configure_ocean_bt`) and device-mapped in
         !! `ocean_dyn_enter_data_impl` — but ONLY when the `accel_visc_rem`
         !! knob is on (unallocated / zero-footprint on the default-off
         !! path); released in destroy/exit_data.

      ! ---- Ghost-band poison (debug) ----
      logical :: poison_ghosts = .false.
         !! When `.true.`, sentinel-NaN the exchange-covered ghost bands at
         !! each outer-step start (before any exchange or kernel).  Any kernel
         !! consuming an unexchanged ghost produces a loud NaN at that step.
         !! Default `.false.` = bit-identical (untaken branch per step).
         !! Driven by `&ocean_mpi_nml poison_ghosts` via `rdb_ocean_setup`.
         !! Requires the `bc` optional argument (edge topology); no-op when
         !! `bc` is absent.

      ! ---- Thermodynamic switch ----
      logical :: enable_thermodynamics = .true.
         !! Mirrors MOM6 `ENABLE_THERMODYNAMICS`.  When `.false.` the step
         !! skips EOS, tracer hdiff, tracer vertical advection/diffusion,
         !! KPP non-local transport, and surface tracer flux.  Tracers
         !! still advect horizontally but with `alpha_T = beta_S = 0` can't
         !! influence dynamics ⇒ adiabatic.  Wire via
         !! `&ocean_setup_nml ocean_enable_thermodynamics`.

      ! ---- Phase-2/3 Lagrangian grounding-stability knobs ----
      real(wp) :: angstrom_h = 0.0_wp
         !! Phase-2/3 copy of `cfg%ocean%isopycnal%angstrom_h` (m).
         !! Used to compute `isopycnal_vanish_tol` for the reset and CFL
         !! gates; 0 ⇒ off ⇒ bit-identical.
      logical :: reset_vanished_u = .false.
         !! Phase 2: when `.true.` AND vcoord is VCOORD_LAGRANGIAN, call
         !! `reset_vanished_layer_velocities` after each
         !! `mask_layer_velocities` (per-stage + RK2-averaged site) to
         !! zero face velocities where BOTH adjacent cell-thicknesses are
         !! at or below `isopycnal_vanish_tol(angstrom_h)`.
      logical :: cfl_ignore_vanished = .false.
         !! Phase 3: when `.true.` AND vcoord is VCOORD_LAGRANGIAN, pass
         !! `vanish_tol` to `compute_max_cfl` and `apply_velocity_truncation`
         !! so vanished-layer face spikes are excluded from MaxCFL /
         !! panic and are zeroed (not CFL-clipped) by truncation.
      logical :: check_h_positive = .false.
         !! DEBUG copy of `cfg%ocean%isopycnal%check_h_positive`.  When
         !! `.true.`, `check_h_positive_or_die` runs after each `h_layer`-
         !! writing stage and aborts on the first negative thickness, naming
         !! the stage.  `.false.` ⇒ never called ⇒ bit-identical.
      ! ---- Wide-halo BT march-in (Phase 3c) ----
      integer :: bt_halo = 0
         !! Wide-halo march-in width (0 = per-substep v1, bit-identical).
         !! Set by `ocean_dyn_enable_bt_wide` after `init` and `enter_data`.
      type(bt_wide_t), allocatable :: bt_wide
         !! Wide shadow state; allocated only when `bt_halo > 0`.

      ! ---- Ideal-age tracer (PR-7) ----
      real(wp) :: ideal_age_young_val = 0.0_wp
         !! Surface-band age value (s), set by `configure_ocean_tracers`
         !! from `&ocean_tracers_nml ideal_age_young_val`.  0 (default) =
         !! today's hard-coded zero reset ⇒ bit-identical.
      real(wp) :: ideal_age_sfc_growth_rate = 0.0_wp
         !! Exponential growth rate of the surface value (1/s), set by
         !! `configure_ocean_tracers` from
         !! `&ocean_tracers_nml ideal_age_sfc_growth_rate`.  0 (default)
         !! ⇒ young_val constant ⇒ no `exp()` ⇒ bit-identical.
   contains
      procedure, non_overridable :: init => ocean_dyn_init
      procedure, non_overridable :: destroy => ocean_dyn_destroy
      procedure, non_overridable :: enter_data => ocean_dyn_enter_data
      procedure, non_overridable :: exit_data => ocean_dyn_exit_data
      procedure, non_overridable :: is_thermo_step => ocean_dyn_is_thermo_step
      procedure, non_overridable :: therm_dt => ocean_dyn_therm_dt
      procedure, non_overridable :: is_tracer_advect_step => ocean_dyn_is_tracer_advect_step
      procedure, non_overridable :: bytes => ocean_dyn_bytes
   end type ocean_dyn_t