ocean_state_build_restart_registry Subroutine

public subroutine ocean_state_build_restart_registry(state, grid, reg)

Walk the ocean god state and register every field that must checkpoint for a bit-exact step-(N+1) resume (ROADMAP A1).

Registered (always): barotropic prognostics h, u_face_x, v_face_y; multilayer prognostics h_layer, u/v_face_x/y_layer, every tracers(:)%hTr (registry-of-registries — iterate the tracer registry so passive tracers join automatically). Registered (conditionally — only when the slot’s persistent array is allocated): vmix%bl_depth — KPP’s lagged BL-depth seed; the V_t² wstar3 term reads the PREVIOUS step’s value, so a restart between steps reads it stale unless checkpointed. Allocated unconditionally (alloc tracks slot existence, not whether the closure is active), so it always registers. epbl%mld + epbl%kd_int, kshear%kd_int + kshear%tke_int. These refresh only at thermo cadence but are merged every stage, so a restart between refreshes reads them stale. Also allocated unconditionally — register-always; the enable-state itself is fixed at config and validated by the schema metadata, so a flipped flag is a fresh run, not a resume (flags cannot flip mid-run). ml_rho_layer — diagnosed each stage from S/T, BUT when dt_therm_ratio > 1 it is NOT recomputed on the slow steps that skip the EOS, so the value carried into the next step depends on the last thermo step. Cheapest correct fix: checkpoint it (review #2). bc%eta_old_chapman_{w,e,s,n} — Chapman radiation persistent corner state (host scalars). bc%tres_* tracer reservoirs — registered when allocated (device-mapped, so they join the update-self walk).

Deliberately EXCLUDED: sf%Q_heat / sf%Q_salt with the surface-flux component set OFF (default) — configure-static: re-seeded from &ocean_thermo_nml q_heat / q_salt on every resume, and the sea-ice couplers’ contributions are folded back in from the registered ice_salt_flux_diag / ice_heat_flux_diag / ice_sw_thru_diag (engine_setup), exactly the sums the couplers form. With the component set ON they ARE registered (sf_Q_heat/sf_Q_salt, see below): ocean_surface_flux_assemble rebuilds them at the END of each thermo step, and every step until the next one reads that assembly – carried state, with SST-dependent terms no configure-time re-assembly could reproduce. RK saves (h_layer0, hTr0, layer0) + bt accumulators (bt_eta/ubt/…) — step-internal scratch, re-zeroed at the top of every outer step (restart is step-aligned). bt_H_ref — recomputed from b by configure_ocean_bt_split; b is bathymetry (static), reconstructed at config time. metrics%z_draft / cover_frac / p_ice_ref — the ice-shelf cavity statics (&ocean_cavity_dyn_nml) fall under the SAME rule, and for the same reason: the draft is a PRESCRIBED, static geometry, rebuilt at configure by seed_cavity_draft from the namelist before the restart read runs, exactly as b and bt_H_ref are. Checkpointing it would create the file-vs-namelist ambiguity the derived-field exclusion exists to avoid (does a saved draft beat an edited namelist?), and a resume whose draft disagreed with its datum would be silently wrong — bt_H_ref = b − z_draft ties the two together, so they must be rebuilt together or not at all. A TIME-VARYING draft (a coupled ice sheet) is a different field with a different owner and would register itself, the same way a time-varying surface-flux component must. vcoord target_h / z_ref — recomputed per step (z_ref rebuilt from b in the seed); never prognostic. w_interface, mass_flux — diagnosed each stage from the prognostics before first use. diag accumulators — MEAN/MAX windows RESET on restart by design (documented); the bit-exact gate covers the prognostic trajectory, not mid-window diag aggregates.

The dead ocean_obc_t scaffold (state%obc) is NOT registered: it is never enabled, never device-mapped, and has no enter_data — registering it (and issuing update self on its host-only buffers) was a latent GPU crash (review #3/#8). When OBC lands, it registers its own live persistent state here.

Arguments

Type IntentOptional Attributes Name
type(ocean_state_t), intent(in), target :: state
type(hgrid_t), intent(in) :: grid
type(restart_registry_t), intent(inout) :: reg

Calls

proc~~ocean_state_build_restart_registry~~CallsGraph proc~ocean_state_build_restart_registry ocean_state_build_restart_registry proc~register_full_3d register_full_3d proc~ocean_state_build_restart_registry->proc~register_full_3d proc~register_full_3d_opt register_full_3d_opt proc~ocean_state_build_restart_registry->proc~register_full_3d_opt proc~registry_clear restart_registry_t%registry_clear proc~ocean_state_build_restart_registry->proc~registry_clear proc~registry_register_2d restart_registry_t%registry_register_2d proc~ocean_state_build_restart_registry->proc~registry_register_2d proc~registry_register_3d restart_registry_t%registry_register_3d proc~ocean_state_build_restart_registry->proc~registry_register_3d proc~registry_register_scalar restart_registry_t%registry_register_scalar proc~ocean_state_build_restart_registry->proc~registry_register_scalar to_string to_string proc~ocean_state_build_restart_registry->to_string proc~register_full_3d->proc~registry_register_3d proc~register_full_3d_opt->proc~registry_register_3d proc~registry_register_2d->to_string error error proc~registry_register_2d->error proc~registry_register_3d->to_string proc~registry_register_3d->error proc~registry_register_scalar->to_string proc~registry_register_scalar->error

Called by

proc~~ocean_state_build_restart_registry~~CalledByGraph proc~ocean_state_build_restart_registry ocean_state_build_restart_registry proc~ocean_state_restart_read ocean_state_restart_read proc~ocean_state_restart_read->proc~ocean_state_build_restart_registry proc~ocean_state_restart_write ocean_state_restart_write proc~ocean_state_restart_write->proc~ocean_state_build_restart_registry proc~ocean_state_restart_write_drop_field ocean_state_restart_write_drop_field proc~ocean_state_restart_write_drop_field->proc~ocean_state_build_restart_registry proc~driver_run_ocean driver_run_ocean proc~driver_run_ocean->proc~ocean_state_restart_write proc~engine_setup engine_setup proc~driver_run_ocean->proc~engine_setup proc~engine_setup->proc~ocean_state_restart_read proc~complete_ocean_create complete_ocean_create proc~complete_ocean_create->proc~engine_setup proc~driver_run driver_run proc~driver_run->proc~driver_run_ocean proc~driver_validate driver_validate proc~driver_validate->proc~engine_setup proc~rdb_ocean_create_finalize rdb_ocean_create_finalize proc~rdb_ocean_create_finalize->proc~complete_ocean_create proc~rdb_ocean_create_from_string rdb_ocean_create_from_string proc~rdb_ocean_create_from_string->proc~complete_ocean_create

Variables

Type Visibility Attributes Name Initial
integer, private :: it
character(len=64), private :: tag

Source Code

   subroutine ocean_state_build_restart_registry(state, grid, reg)
      !! Walk the ocean god state and register every field that must
      !! checkpoint for a bit-exact step-(N+1) resume (ROADMAP A1).
      !!
      !! Registered (always):
      !!   barotropic prognostics h, u_face_x, v_face_y;
      !!   multilayer prognostics h_layer, u/v_face_x/y_layer, every
      !!   tracers(:)%hTr (registry-of-registries — iterate the tracer
      !!   registry so passive tracers join automatically).
      !! Registered (conditionally — only when the slot's persistent
      !!   array is allocated):
      !!   vmix%bl_depth — KPP's lagged BL-depth seed; the V_t² wstar3
      !!     term reads the PREVIOUS step's value, so a restart between
      !!     steps reads it stale unless checkpointed.  Allocated
      !!     unconditionally (alloc tracks slot existence, not whether the
      !!     closure is active), so it always registers.
      !!   epbl%mld + epbl%kd_int, kshear%kd_int + kshear%tke_int.
      !!     These refresh only at thermo cadence but are *merged every
      !!     stage*, so a restart between refreshes reads them stale.
      !!     Also allocated unconditionally — register-always; the
      !!     enable-state itself is fixed at config and validated by the
      !!     schema metadata, so a flipped flag is a fresh run, not a
      !!     resume (flags cannot flip mid-run).
      !!   ml_rho_layer — diagnosed each stage from S/T, BUT when
      !!     `dt_therm_ratio > 1` it is NOT recomputed on the slow steps
      !!     that skip the EOS, so the value carried into the next step
      !!     depends on the last thermo step.  Cheapest correct fix:
      !!     checkpoint it (review #2).
      !!   bc%eta_old_chapman_{w,e,s,n} — Chapman radiation persistent
      !!     corner state (host scalars).  bc%tres_* tracer reservoirs —
      !!     registered when allocated (device-mapped, so they join the
      !!     update-self walk).
      !!
      !! Deliberately EXCLUDED:
      !!   sf%Q_heat / sf%Q_salt with the surface-flux component set OFF
      !!     (default) — configure-static: re-seeded from `&ocean_thermo_nml
      !!     q_heat / q_salt` on every resume, and the sea-ice couplers'
      !!     contributions are folded back in from the registered
      !!     `ice_salt_flux_diag` / `ice_heat_flux_diag` / `ice_sw_thru_diag`
      !!     (`engine_setup`), exactly the sums the couplers form.  With the
      !!     component set ON they ARE registered (`sf_Q_heat`/`sf_Q_salt`,
      !!     see below): `ocean_surface_flux_assemble` rebuilds them at the
      !!     END of each thermo step, and every step until the next one
      !!     reads that assembly -- carried state, with SST-dependent terms
      !!     no configure-time re-assembly could reproduce.
      !!   RK saves (h_layer0, hTr0, *_layer0) + bt accumulators
      !!     (bt_eta/ubt/...) — step-internal scratch, re-zeroed at the
      !!     top of every outer step (restart is step-aligned).
      !!   bt_H_ref — recomputed from `b` by configure_ocean_bt_split;
      !!     `b` is bathymetry (static), reconstructed at config time.
      !!   metrics%z_draft / cover_frac / p_ice_ref — the ice-shelf cavity
      !!     statics (`&ocean_cavity_dyn_nml`) fall under the SAME rule,
      !!     and for the same reason: the draft is a PRESCRIBED, static
      !!     geometry, rebuilt at configure by `seed_cavity_draft` from
      !!     the namelist before the restart read runs, exactly as `b` and
      !!     `bt_H_ref` are.  Checkpointing it would create the
      !!     file-vs-namelist ambiguity the derived-field exclusion exists
      !!     to avoid (does a saved draft beat an edited namelist?), and a
      !!     resume whose draft disagreed with its datum would be
      !!     silently wrong — `bt_H_ref = b − z_draft` ties the two
      !!     together, so they must be rebuilt together or not at all.
      !!     A TIME-VARYING draft (a coupled ice sheet) is a different
      !!     field with a different owner and would register itself, the
      !!     same way a time-varying surface-flux component must.
      !!   vcoord target_h / z_ref — recomputed per step (z_ref rebuilt
      !!     from `b` in the seed); never prognostic.
      !!   w_interface, mass_flux_* — diagnosed each stage from the
      !!     prognostics before first use.
      !!   diag accumulators — MEAN/MAX windows RESET on restart by
      !!     design (documented); the bit-exact gate covers the
      !!     prognostic trajectory, not mid-window diag aggregates.
      !!
      !! The dead `ocean_obc_t` scaffold (`state%obc`) is NOT registered:
      !! it is never enabled, never device-mapped, and has no enter_data —
      !! registering it (and issuing `update self` on its host-only
      !! buffers) was a latent GPU crash (review #3/#8).  When OBC lands,
      !! it registers its own live persistent state here.
      type(ocean_state_t), intent(in), target :: state
      type(hgrid_t), intent(in) :: grid
      type(restart_registry_t), intent(inout) :: reg

      integer :: it
      character(len=64) :: tag

      call reg%clear()

      ! Ghost-cell policy (v1): the structured prognostic + closure
      ! arrays are checkpointed as FULL local arrays (interior + ghosts),
      ! registered with ng=0 over the total extent.  Rationale — three
      ! kinds of ghost cell, distinguished for the MPI-readiness contract:
      !   * physical domain-edge (wall) ghosts are GENUINE owned boundary
      !     state — the slow-tendency operators (EOS / face-thickness /
      !     hvisc) read `h_layer` etc. at wall-adjacent ghost faces and
      !     there is NO stage-entry wall-mirror op to re-establish them,
      !     so owned-only loses information and breaks the bit-exact gate.
      !   * periodic / tripolar-fold seam ghosts are redundant — the
      !     stage-entry wrap (`ocean_periodic_wrap_state` / fold) rebuilds
      !     them from the interior every step; saving them is harmless.
      !   * inter-rank halo ghosts (E1, multi-rank) will be refilled by
      !     the halo exchange on resume; under the v1 SAME-decomp rule
      !     those columns round-trip identically anyway.
      ! So a full-array write is bit-exact today and forward-compatible
      ! with E1 (the halo columns simply get overwritten by exchange).
      ! The owned-slice machinery in restart_entry_t is retained for the
      ! ghostless edge buffers (OBC rings) and for a future owned-only
      ! mode once a wall-ghost-rebuild op exists.
      ! --- Barotropic prognostics ---
      call reg%register_2d("bt_h", state%barotropic%h, 0, &
                           size(state%barotropic%h, 1), size(state%barotropic%h, 2))
      call reg%register_2d("bt_u_face_x", state%barotropic%u_face_x, 0, &
                           size(state%barotropic%u_face_x, 1), size(state%barotropic%u_face_x, 2))
      call reg%register_2d("bt_v_face_y", state%barotropic%v_face_y, 0, &
                           size(state%barotropic%v_face_y, 1), size(state%barotropic%v_face_y, 2))

      if (state%use_multilayer) then
         ! --- Multilayer prognostics ---
         call register_full_3d(reg, "ml_h_layer", state%multilayer%h_layer)
         call register_full_3d(reg, "ml_u_face_x_layer", state%multilayer%u_face_x_layer)
         call register_full_3d(reg, "ml_v_face_y_layer", state%multilayer%v_face_y_layer)

         ! --- Tracers (registry-of-registries) ---
         if (allocated(state%multilayer%tracers)) then
            do it = 1, size(state%multilayer%tracers)
               write (tag, "(A,I0)") "tracer_hTr_", it
               call register_full_3d(reg, trim(tag), state%multilayer%tracers(it)%hTr)
            end do
         end if
         ! --- pred_corr step time-means (SPEC S1).  Under
         !     `&ocean_bt_nml split_scheme="pred_corr"` these are CARRIED
         !     STATE, not scratch: the predictor's Coriolis-advection and
         !     horizontal viscosity read u_av/v_av/h_av, and the dyn loop
         !     seeds them from the initial state only at
         !     `outer_step_count == 0`.  A resume restores outer_step_count
         !     from the file, so that seed does NOT re-fire -- without
         !     checkpointing them the first post-restart predictor reads
         !     the allocation zeros (a zero-thickness h_av) and the resume
         !     is not bit-exact.  Caught by `restart_bit_exact_*` the day
         !     pred_corr became the default.  `optional=.true.`: a pre-flip
         !     checkpoint has no such variables and must still resume --
         !     it will be an ssp_rk2 file, where they are unread.
         if (allocated(state%multilayer%u_av_layer)) then
            call reg%register_3d("ml_u_av_layer", state%multilayer%u_av_layer, 0, &
                                 size(state%multilayer%u_av_layer, 1), &
                                 size(state%multilayer%u_av_layer, 2), optional=.true.)
         end if
         if (allocated(state%multilayer%v_av_layer)) then
            call reg%register_3d("ml_v_av_layer", state%multilayer%v_av_layer, 0, &
                                 size(state%multilayer%v_av_layer, 1), &
                                 size(state%multilayer%v_av_layer, 2), optional=.true.)
         end if
         if (allocated(state%multilayer%h_av_layer)) then
            call reg%register_3d("ml_h_av_layer", state%multilayer%h_av_layer, 0, &
                                 size(state%multilayer%h_av_layer, 1), &
                                 size(state%multilayer%h_av_layer, 2), optional=.true.)
         end if
         ! --- pred_corr carried viscous tendency.  The predictor does NOT
         !     recompute the lateral viscosity: it reuses the previous
         !     step's corrector `du_visc`/`dv_visc` (MOM6 `diffu(u[n-1])`,
         !     `rdb_ocean_dyn`'s `is_pred` gate), so these buffers are
         !     CARRIED STATE under pred_corr, not scratch.  Unregistered,
         !     the first post-restart predictor read the device-zeroed
         !     payload -- one step with no lateral viscosity in any column
         !     -- and the resume was not bit-exact (found on the 1/4-degree
         !     Southern Ocean, 2026-10-01; the restart gate ran inviscid).
         !     Under ssp_rk2 they are recomputed before every read, so
         !     restoring them is inert.  `optional=.true.`: an older
         !     checkpoint still resumes, with the old one-step defect.
         if (allocated(state%hvisc%du_visc%data)) then
            call register_full_3d_opt(reg, "hvisc_du_visc", state%hvisc%du_visc%data)
         end if
         if (allocated(state%hvisc%dv_visc%data)) then
            call register_full_3d_opt(reg, "hvisc_dv_visc", state%hvisc%dv_visc%data)
         end if
      end if

      ! --- rho_layer (review #2): diagnosed each stage, but NOT
      !     recomputed on dt_therm-skipped slow steps, so the value
      !     carried forward is thermo-step-dependent.  Checkpoint it. ---
      if (state%use_multilayer .and. allocated(state%multilayer%rho_layer)) then
         call register_full_3d(reg, "ml_rho_layer", state%multilayer%rho_layer)
      end if

      ! --- KPP lagged BL-depth seed (review #1): the V_t² wstar3_lagged
      !     term reads the previous step's bl_depth.  Allocated
      !     unconditionally; required for a bit-exact KPP resume. ---
      if (allocated(state%vmix%bl_depth)) then
         call reg%register_2d("vmix_bl_depth", state%vmix%bl_depth, 0, &
                              size(state%vmix%bl_depth, 1), size(state%vmix%bl_depth, 2))
      end if

      ! --- PR-2 (bt-rem-from-av-rem): vmix%kv, closing the REAL root cause
      !     of compat row `restart_visc_rem`.  `visc_rem_precompute` runs
      !     at the START of every pred_corr stage (MOM6-order parity,
      !     PGF_BUG.md §9), BEFORE that stage's own `vmix_apply_in_stage`
      !     recomputes `vmix%kv` -- so it always reads the PREVIOUS
      !     stage's `kv`, one stage stale by design (mirrors MOM6:
      !     `vertvisc_coef`'s `visc%Kv_slow`/the shear-viscosity input is
      !     likewise carried across the predictor/corrector boundary, and
      !     MOM6 checkpoints exactly this class of field --
      !     `set_visc_register_restarts`
      !     registers `Kv_shear`/`Kd_shear`/`Kv_shear_Bu`/`MLD` for the
      !     same "read before recomputed" reason).  Unregistered, a warm
      !     restart re-seeds `kv` from the COLD background value
      !     (`vmix_seed_backgrounds`, `pp81_nu_bg`) rather than the
      !     spun-up profile, so the first resumed stage's visc_rem matrix
      !     (hence `F_bt`'s weighting under `bt_forcing_visc_rem`/
      !     `bt_renorm_visc_rem`, and `bt_rem_from_visc_rem`'s av_rem/
      !     bt_rem) differs from the continued run --
      !     every prognostic then drifts (measured 1e-11 relative by step
      !     24, `tests/regression/compat_expect.py::restart_visc_rem`).
      !     `kv` is allocated UNCONDITIONALLY by `ocean_vmix_init`
      !     (every vmix consumer shares it, not just visc_rem), so this
      !     registers on every run, not just visc_rem-chain ones --
      !     cheap and simple beats a consumer-flag gate here: one extra
      !     `(nx, ny, nz+1)` field, the same class of cost as
      !     `epbl_kd_int` two lines below.  `optional=.true.`: a
      !     pre-PR-2 checkpoint has no such field and must still resume
      !     (re-seeding `kv` cold, the pre-existing one-stage-stale
      !     defect, same compat posture as `bt_visc_rem_u/v` above). ---
      if (allocated(state%vmix%kv)) then
         call register_full_3d_opt(reg, "vmix_kv", state%vmix%kv)
      end if

      ! --- EPBL prev-MLD seed + kd_int (merged every stage) ---
      if (allocated(state%epbl%mld)) then
         call reg%register_2d("epbl_mld", state%epbl%mld, 0, &
                              size(state%epbl%mld, 1), size(state%epbl%mld, 2))
      end if
      if (allocated(state%epbl%kd_int)) then
         call register_full_3d(reg, "epbl_kd_int", state%epbl%kd_int)
      end if

      ! --- MEKE prognostic eddy-energy field (capability [5]): without
      !     this it would cold-reset to 0 each restart, losing the spun-up
      !     eddy field.  Allocated whenever the multilayer path is on. ---
      if (allocated(state%meke%meke)) then
         call reg%register_2d("meke", state%meke%meke, 0, &
                              size(state%meke%meke, 1), size(state%meke%meke, 2))
      end if

      ! --- Gent-McWilliams carried state.  The GM operator runs AFTER the
      !     dynamics of step n (`run_gm_step`) and leaves its PE release
      !     `gm_src`, which MEKE reads at the top of step n+1: carried, so
      !     a resume between the two must restore it (without it the first
      !     resumed MEKE step sourced from a cold GM work, ~3 % off at step
      !     24).  With `dt_therm_ratio > 1` the GM operator also runs on
      !     the steps BETWEEN thermo refreshes, reading the slopes and the
      !     VarMix(+MEKE) base KhTh the last thermo step left — carried as
      !     well (at ratio 1 they are recomputed before every read, so
      !     restoring them is inert).  Registered on EVERY GM run: this
      !     registry is built for the read BEFORE `configure_ocean_vmix`
      !     sets `dyn%dt_therm_ratio`, so the ratio cannot gate it (a
      !     ratio-gated registration wrote them and never read them back —
      !     `restart_engine_bit_exact_gm_meke_varmix_therm2`).  All
      !     optional: a checkpoint written before they were registered
      !     still resumes, with the old one-step defect. ---
      if (state%gm%enable .and. allocated(state%gm%gm_src)) then
         call reg%register_2d("gm_src", state%gm%gm_src, 0, &
                              size(state%gm%gm_src, 1), size(state%gm%gm_src, 2), optional=.true.)
         if (allocated(state%slopes%slope_x)) then
            call register_full_3d_opt(reg, "slopes_slope_x", state%slopes%slope_x)
            call register_full_3d_opt(reg, "slopes_slope_y", state%slopes%slope_y)
            call register_full_3d_opt(reg, "slopes_n2_u", state%slopes%n2_u)
            call register_full_3d_opt(reg, "slopes_n2_v", state%slopes%n2_v)
         end if
         if (state%varmix%enable .and. allocated(state%varmix%khth_u)) then
            call reg%register_2d("varmix_khth_u", state%varmix%khth_u, 0, &
                                 size(state%varmix%khth_u, 1), size(state%varmix%khth_u, 2), &
                                 optional=.true.)
            call reg%register_2d("varmix_khth_v", state%varmix%khth_v, 0, &
                                 size(state%varmix%khth_v, 1), size(state%varmix%khth_v, 2), &
                                 optional=.true.)
         end if
      end if

      ! --- Wet/dry hysteresis mask (docs/ocean_wetdry_plan.md): persistent
      !     front state (wet/dry/held-in-band).  optional — a restart
      !     written before the knob existed re-seeds from depth at
      !     configure instead of failing. ---
      if (allocated(state%dyn%bt_work%wd_wet_dyn)) then
         call reg%register_2d("wd_wet_dyn", state%dyn%bt_work%wd_wet_dyn, 0, &
                              size(state%dyn%bt_work%wd_wet_dyn, 1), &
                              size(state%dyn%bt_work%wd_wet_dyn, 2), optional=.true.)
      end if

      ! --- PR-1 viscous remnant γ (`bt_work%visc_rem_u/v`): REFRESHED,
      !     not re-derived from scratch, by the stage-end vdiff producer
      !     (`bt_visc_rem_producer`, D1 follow-up -- decoupled from the
      !     retired `bt_correction_visc_rem`) and the pre-substep
      !     `visc_rem_precompute` (`forcing_visc_rem`/`renorm_visc_rem`/
      !     `bt_rem_from_visc_rem`) -- a cold resume without this
      !     checkpoint would restart every post-restart stage from the
      !     `source=1.0` init value, which is one stage's worth of
      !     refresh behind a continued run (consumers read the PREVIOUS
      !     stage's γ by design -- see `bt_visc_rem_producer`'s
      !     docstring in `rdb_barotropic_workstate`).  Allocated
      !     unconditionally by
      !     `barotropic_workstate_t%init`.  `optional=.true.`: a pre-PR-1
      !     checkpoint has no such variables and must still resume -- it
      !     re-seeds at 1.0, same as a cold start (closes compat row
      !     `restart_visc_rem`). ---
      if (allocated(state%dyn%bt_work%visc_rem_u)) then
         call register_full_3d_opt(reg, "bt_visc_rem_u", state%dyn%bt_work%visc_rem_u)
      end if
      if (allocated(state%dyn%bt_work%visc_rem_v)) then
         call register_full_3d_opt(reg, "bt_visc_rem_v", state%dyn%bt_work%visc_rem_v)
      end if

      ! --- Sea-ice frazil bank (PR 1): un-spent supercooling heat the ice
      !     model (PR 3) will consume.  PERSISTENT (accumulates across
      !     steps), so a restart must carry it or banked energy would be
      !     silently discarded.  optional — only ice-on runs write it, and
      !     an ice-on resume from an older checkpoint re-seeds 0. ---
      if (allocated(state%ice%frazil_heat)) then
         call reg%register_2d("ice_frazil_heat", state%ice%frazil_heat, 0, &
                              size(state%ice%frazil_heat, 1), &
                              size(state%ice%frazil_heat, 2), optional=.true.)
      end if

      ! --- Sea-ice PR 3b: brine-rejection flux diag.  `salt_flux_diag` is
      !     REQUIRED for restart-exact Q_salt refill: `rdb_ice_ocean_coupler`
      !     rebuilds Q_salt from this field every 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 (Q_salt itself is configure-static and NOT
      !     registered).  `m_frozen_diag` is registered alongside for
      !     post-resume diagnostic continuity only.  optional — an
      !     ice-on resume from a pre-3b checkpoint re-seeds 0 (a one-window
      !     cold-start of the brine flux) rather than failing.
      !
      !     PR 3c extends the same contract to the melt side:
      !     `heat_flux_diag` is REQUIRED for restart-exact Q_heat refill
      !     (mirrors `salt_flux_diag` — `ice_ocean_heat_flux` rebuilds
      !     Q_heat from it every thermo step, and the driver resume fold
      !     re-applies it); `m_melt_diag` is diagnostic-continuity only,
      !     like `m_frozen_diag`.  PR 31 extends the same required-refill
      !     contract to `sw_thru_diag` — `ice_ocean_sw_flux` rebuilds the
      !     ocean shortwave from it every thermo step and the driver resume
      !     fold re-applies it, so it is registered alongside
      !     `heat_flux_diag`.  The coupleable atmospheric-forcing seam
      !     (`atm_sf0`/`atm_dsfdt`/`atm_sw_dn`) and the column-driver
      !     scratch (`fb`/`sst_seam`/`ssurf_seam`/`tfw_seam`/`tsurf_out`/
      !     `h2o_ocn_to_ice`/`h2o_ice_to_ocn`/`heat_to_ocn`/`sw_thru`) are
      !     ALL recomputed every thermo step — deliberately NOT
      !     registered (the per-category `sw_thru` stays unregistered; only
      !     its per-cell reduction `sw_thru_diag` is carried). ---
      if (allocated(state%ice%m_frozen_diag)) then
         call reg%register_2d("ice_m_frozen_diag", state%ice%m_frozen_diag, 0, &
                              size(state%ice%m_frozen_diag, 1), &
                              size(state%ice%m_frozen_diag, 2), optional=.true.)
      end if
      if (allocated(state%ice%salt_flux_diag)) then
         call reg%register_2d("ice_salt_flux_diag", state%ice%salt_flux_diag, 0, &
                              size(state%ice%salt_flux_diag, 1), &
                              size(state%ice%salt_flux_diag, 2), optional=.true.)
      end if
      if (allocated(state%ice%m_melt_diag)) then
         call reg%register_2d("ice_m_melt_diag", state%ice%m_melt_diag, 0, &
                              size(state%ice%m_melt_diag, 1), &
                              size(state%ice%m_melt_diag, 2), optional=.true.)
      end if
      if (allocated(state%ice%heat_flux_diag)) then
         call reg%register_2d("ice_heat_flux_diag", state%ice%heat_flux_diag, 0, &
                              size(state%ice%heat_flux_diag, 1), &
                              size(state%ice%heat_flux_diag, 2), optional=.true.)
      end if
      if (allocated(state%ice%sw_thru_diag)) then
         call reg%register_2d("ice_sw_thru_diag", state%ice%sw_thru_diag, 0, &
                              size(state%ice%sw_thru_diag, 1), &
                              size(state%ice%sw_thru_diag, 2), optional=.true.)
      end if

      ! --- Ice-shelf cavity basal melt (P2b): the TWO OWNED surface-flux
      !     components.  These ARE registered, and the exclusion rule
      !     above says exactly why: `Q_heat`/`Q_salt` are derived views
      !     and stay out, but "any filler that makes a COMPONENT
      !     time-varying MUST register it".  The melt rate is a function
      !     of the live state, so `heat_cavity`/`salt_cavity` are
      !     time-varying — and they are written at the END of outer step
      !     N and integrated on step N+1 (the ice coupler's documented
      !     one-step lag), so without them a warm restart would apply
      !     zero melt for its first step.  `optional=.true.`: an older
      !     checkpoint resumes with the zero seed rather than failing.
      !     The slot's own arrays (`cavity_flux%melt`, `t_b`, ...) are
      !     NOT registered — they are recomputed from state before first
      !     use, the same derived-field rule as `mass_flux_*`.
      if (allocated(state%surface_flux%heat_cavity)) then
         call reg%register_2d("sf_heat_cavity", state%surface_flux%heat_cavity, 0, &
                              size(state%surface_flux%heat_cavity, 1), &
                              size(state%surface_flux%heat_cavity, 2), optional=.true.)
      end if
      if (allocated(state%surface_flux%salt_cavity)) then
         call reg%register_2d("sf_salt_cavity", state%surface_flux%salt_cavity, 0, &
                              size(state%surface_flux%salt_cavity, 1), &
                              size(state%surface_flux%salt_cavity, 2), optional=.true.)
      end if

      ! --- Assembled net surface fluxes under the component set.  With
      !     `use_components` the assembler derives Q_heat/Q_salt at the END
      !     of each thermo step and the following steps read them, so they
      !     are carried state (an end-of-window checkpoint is read by the
      !     very next step); `sf_q_assembled` says the arrays hold an
      !     assembly (engine_setup then resumes them instead of the
      !     configure seed + ice fold, which sum the same terms in a
      !     different order and miss every non-ice component).  Optional:
      !     an older checkpoint resumes with the seed + fold as before.
      !     `set_components` runs BEFORE the restart read in engine_setup,
      !     so these (and sf_heat_cavity/sf_salt_cavity above) exist when
      !     the read walks the registry. ---
      if (state%surface_flux%use_components) then
         call reg%register_2d("sf_Q_heat", state%surface_flux%Q_heat, 0, &
                              size(state%surface_flux%Q_heat, 1), &
                              size(state%surface_flux%Q_heat, 2), optional=.true.)
         call reg%register_2d("sf_Q_salt", state%surface_flux%Q_salt, 0, &
                              size(state%surface_flux%Q_salt, 1), &
                              size(state%surface_flux%Q_salt, 2), optional=.true.)
         call reg%register_scalar("sf_q_assembled", state%surface_flux%q_assembled, &
                                  optional=.true.)
      end if

      ! --- Sea-ice PR 5: C-grid EVP dynamics prognostics.  u_ice/v_ice
      !     become PROGNOSTIC under dynamics=.true. (previously a v1
      !     interim sampler output — cheap to always register).
      !     str_d/str_t/str_s are the H&D stress tensor components;
      !     fxoc/fyoc are carried for DIAGNOSTIC CONTINUITY only (PR 63 —
      !     see below, they are no longer what makes tau_x/tau_y
      !     restart-exact).  All optional: a resume from a pre-5
      !     checkpoint re-seeds 0 (a one-window cold-start of the ice
      !     velocity/stress) rather than failing.  tau_a_x/tau_a_y are
      !     deliberately NOT registered (configure-time snapshot, rebuilt
      !     fresh from the wind-stress config on every run).
      !
      !     PR 63: tau_ocn_x/tau_ocn_y + tau_ocn_valid are what make the
      !     ice->ocean tau MEDIATION restart-exact.  ice_ocean_stress_flux
      !     mirrors its own blended output into tau_ocn_x/y every outer
      !     step; ice_ocean_stress_resume_apply COPIES them into
      !     surface_stress%tau_x/y at configure (after the wind re-seed),
      !     replacing the old deleted resume-fold RECONSTRUCT routine
      !     (which used the checkpoint's POST-thermo ci — wrong whenever a
      !     checkpoint step's thermo/transport changed ci after the blend,
      !     F4).  fxoc/fyoc above are no longer load-bearing for this: they
      !     are write-only-within-a-call accumulators
      !     (`ice_evp_dynamics` zeros them at entry), so the resume fold
      !     was their only cross-step-boundary reader — that reader is
      !     gone now.  tau_ocn_valid is register_scalar (device_mapped
      !     forced .false.) and gated on the SAME allocated(...) check as
      !     the arrays, so an ocean-only restart file never grows this
      !     variable. ---
      if (allocated(state%ice%u_ice)) then
         call reg%register_2d("ice_u_ice", state%ice%u_ice, 0, &
                              size(state%ice%u_ice, 1), &
                              size(state%ice%u_ice, 2), optional=.true.)
      end if
      if (allocated(state%ice%v_ice)) then
         call reg%register_2d("ice_v_ice", state%ice%v_ice, 0, &
                              size(state%ice%v_ice, 1), &
                              size(state%ice%v_ice, 2), optional=.true.)
      end if
      if (allocated(state%ice%str_d)) then
         call reg%register_2d("ice_str_d", state%ice%str_d, 0, &
                              size(state%ice%str_d, 1), &
                              size(state%ice%str_d, 2), optional=.true.)
      end if
      if (allocated(state%ice%str_t)) then
         call reg%register_2d("ice_str_t", state%ice%str_t, 0, &
                              size(state%ice%str_t, 1), &
                              size(state%ice%str_t, 2), optional=.true.)
      end if
      if (allocated(state%ice%str_s)) then
         call reg%register_2d("ice_str_s", state%ice%str_s, 0, &
                              size(state%ice%str_s, 1), &
                              size(state%ice%str_s, 2), optional=.true.)
      end if
      if (allocated(state%ice%fxoc)) then
         call reg%register_2d("ice_fxoc", state%ice%fxoc, 0, &
                              size(state%ice%fxoc, 1), &
                              size(state%ice%fxoc, 2), optional=.true.)
      end if
      if (allocated(state%ice%fyoc)) then
         call reg%register_2d("ice_fyoc", state%ice%fyoc, 0, &
                              size(state%ice%fyoc, 1), &
                              size(state%ice%fyoc, 2), optional=.true.)
      end if
      ! PR 63: the ice->ocean tau mediation seam (see the comment block
      ! above).  The allocated(...) gate is mandatory for all THREE
      ! entries, including the scalar — tau_ocn_valid lives on the type
      ! whether or not the ice slot is live; gating it on the array's
      ! allocation (rather than registering it unconditionally) keeps an
      ! ocean-only restart file free of a stray ice_tau_ocn_valid
      ! variable.
      if (allocated(state%ice%tau_ocn_x)) then
         call reg%register_2d("ice_tau_ocn_x", state%ice%tau_ocn_x, 0, &
                              size(state%ice%tau_ocn_x, 1), &
                              size(state%ice%tau_ocn_x, 2), optional=.true.)
         call reg%register_2d("ice_tau_ocn_y", state%ice%tau_ocn_y, 0, &
                              size(state%ice%tau_ocn_y, 1), &
                              size(state%ice%tau_ocn_y, 2), optional=.true.)
         call reg%register_scalar("ice_tau_ocn_valid", state%ice%tau_ocn_valid, &
                                  optional=.true.)
      end if

      ! --- Sea-ice Winton column prognostics (PR 3a): part_size, m_ice,
      !     m_snow are rank-3 (register directly); enth_ice/sal_ice
      !     (rank-4, per-k bottom-up) and enth_snow (rank-4, nk=1) are
      !     registered as per-k rank-3 slices — the registry is
      !     rank-2/3 only.  All optional: a resume from a pre-3a
      !     checkpoint warns-and-seeds the init values (part_size all
      !     open water, m_ice/m_snow/enth_* = 0, sal_ice =
      !     ICE_BULK_SALINITY) rather than failing. ---
      if (allocated(state%ice%part_size)) then
         call reg%register_3d("ice_part_size", state%ice%part_size, 0, &
                              size(state%ice%part_size, 1), &
                              size(state%ice%part_size, 2), optional=.true.)
      end if
      if (allocated(state%ice%m_ice)) then
         call reg%register_3d("ice_m_ice", state%ice%m_ice, 0, &
                              size(state%ice%m_ice, 1), &
                              size(state%ice%m_ice, 2), optional=.true.)
      end if
      if (allocated(state%ice%m_snow)) then
         call reg%register_3d("ice_m_snow", state%ice%m_snow, 0, &
                              size(state%ice%m_snow, 1), &
                              size(state%ice%m_snow, 2), optional=.true.)
      end if
      if (allocated(state%ice%enth_ice)) then
         do it = 1, size(state%ice%enth_ice, 4)
            call reg%register_3d("ice_enth_ice_k"//to_string(it), &
                                 state%ice%enth_ice(:, :, :, it), 0, &
                                 size(state%ice%enth_ice, 1), &
                                 size(state%ice%enth_ice, 2), optional=.true.)
         end do
      end if
      if (allocated(state%ice%sal_ice)) then
         do it = 1, size(state%ice%sal_ice, 4)
            call reg%register_3d("ice_sal_ice_k"//to_string(it), &
                                 state%ice%sal_ice(:, :, :, it), 0, &
                                 size(state%ice%sal_ice, 1), &
                                 size(state%ice%sal_ice, 2), optional=.true.)
         end do
      end if
      if (allocated(state%ice%enth_snow)) then
         do it = 1, size(state%ice%enth_snow, 4)
            call reg%register_3d("ice_enth_snow_k"//to_string(it), &
                                 state%ice%enth_snow(:, :, :, it), 0, &
                                 size(state%ice%enth_snow, 1), &
                                 size(state%ice%enth_snow, 2), optional=.true.)
         end do
      end if

      ! --- Kappa-shear kd_int / tke_int (merged every stage) ---
      if (allocated(state%kshear%kd_int)) then
         call register_full_3d(reg, "kshear_kd_int", state%kshear%kd_int)
      end if
      if (allocated(state%kshear%tke_int)) then
         call register_full_3d(reg, "kshear_tke_int", state%kshear%tke_int)
      end if

      ! --- Kappa-shear VERTEX corner diffusivity (c069 restart fix):
      !     `kd_corner` is the Pass-B -> Pass-C carrier `kappa_shear_compute`
      !     writes and `vdiff_apply_momentum`'s `kv_corner_source` reads --
      !     but it is only refreshed at STAGE 1 of a thermo step
      !     (`vmix_apply_in_stage`, "stage-invariant by design"), while
      !     `visc_rem_precompute` -- called at the START of every stage,
      !     BEFORE that refresh -- reads it on EVERY stage, including
      !     stage 1 itself (MOM6-order parity, PGF_BUG.md Sec.9:
      !     `vertvisc_coef` runs before `btstep`).  On a continued run
      !     stage 1's `visc_rem_precompute` sees the PREVIOUS step's
      !     converged `kd_corner`, still live in process memory.  On a
      !     warm restart `kd_corner` is a plain workspace -- allocated
      !     zero by `init_vertex`, never written before the first resumed
      !     stage's `visc_rem_precompute` call -- so that FIRST call
      !     silently drops the vertex kappa-shear's corner viscosity from
      !     the remnant matrix, perturbing `bt_visc_rem_u/v` at the
      !     `kappa_trunc` scale (~1e-9) and from there every consumer of
      !     the BT-correction weighting (compat row c069).  `optional
      !     =.true.`: a pre-fix checkpoint has no such field and must
      !     still resume (re-seeding `kd_corner` at 0 for the first
      !     stage only, same as the pre-existing defect). ---
      if (allocated(state%kshear%kd_corner)) then
         call register_full_3d_opt(reg, "kshear_kd_corner", state%kshear%kd_corner)
      end if

      ! --- Live persistent BC state (review #3) ---
      ! Chapman radiation corner scalars: host-side, always present.
      call reg%register_scalar("bc_eta_old_chapman_w", state%bc%eta_old_chapman_w)
      call reg%register_scalar("bc_eta_old_chapman_e", state%bc%eta_old_chapman_e)
      call reg%register_scalar("bc_eta_old_chapman_s", state%bc%eta_old_chapman_s)
      call reg%register_scalar("bc_eta_old_chapman_n", state%bc%eta_old_chapman_n)
      ! Tracer reservoirs: rank-3 (edge, layer, tracer); device-mapped
      ! when allocated, so they join the update-self walk by default.
      if (allocated(state%bc%tres_west)) then
         call register_full_3d(reg, "bc_tres_west", state%bc%tres_west)
      end if
      if (allocated(state%bc%tres_east)) then
         call register_full_3d(reg, "bc_tres_east", state%bc%tres_east)
      end if
      if (allocated(state%bc%tres_south)) then
         call register_full_3d(reg, "bc_tres_south", state%bc%tres_south)
      end if
      if (allocated(state%bc%tres_north)) then
         call register_full_3d(reg, "bc_tres_north", state%bc%tres_north)
      end if
   end subroutine ocean_state_build_restart_registry