Default fill routines for the ocean diagnostics registry.
!! Default fill routines for the ocean diagnostics registry. module rdb_ocean_diag_fills !! Per-variable fill procedures the diag manager invokes on cadence-fire; !! the bridge between the diag manager and `ocean_state_t`. Each fill !! `select type`-casts the `class(*)` state handle back to !! `ocean_state_t` and populates a pre-allocated buffer. Face-staggered !! fields (u_face_x, v_face_y) are averaged to cell centres so every !! default variable is `(nx, ny, nz)`. !! Defaults: SSH (m), temperature (°C), salinity (PSU), u/v_centre (m/s), !! ke (m²/s²); conditionally ice_conc (1) / ice_thick (m) when !! `&ocean_ice_nml enable`. #ifdef LFORTRAN_PASSING use rdb_constants, only: wp, H_VANISHED, & REMAP_PCM, REMAP_PLM, REMAP_PPM, REMAP_PPM_H4 #else use rdb_constants, only: NZ_STACK_MAX, wp, H_VANISHED, & REMAP_PCM, REMAP_PLM, REMAP_PPM, REMAP_PPM_H4 #endif use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan use rdb_ocean_state, only: ocean_state_t use rdb_ocean_diag, only: ocean_diag_t, DIAG_OP_INSTANT, DIAG_OP_MEAN, & DIAG_OP_MAX, DIAG_OP_MIN, DIAG_OP_UNSET, & diag_spec_t, diag_remap_proc, diag_fill_proc, & DIAG_VGRID_LAYER, DIAG_VGRID_Z_FIXED, & DIAG_VGRID_SIGMA, DIAG_VGRID_ZSTAR, & DIAG_VGRID_DENSITY, DIAG_COORD_UNSET, & DIAG_MISSING_VALUE use rdb_eos, only: eos_t, eos_density_point use rdb_ocean_vcoord, only: invert_density_targets use rdb_remap_column, only: remap_column use rdb_ice_column, only: ICE_RHO_ICE use rdb_ocean_pseudo_salt, only: ocean_pseudo_salt_deviation implicit none private #ifdef LFORTRAN_PASSING integer, parameter :: NZ_STACK_MAX = 64 !! LFortran 0.64 workaround: module-local copy of the rdb_constants value !! (an imported parameter used as an explicit-shape dummy bound inside a !! PURE call becomes an impure getter under LFortran). Keep in sync (=64). #endif public :: fill_ssh, fill_temperature, fill_salinity public :: fill_age public :: fill_pseudo_salt, fill_pseudo_salt_diff public :: fill_u_centre, fill_v_centre, fill_ke public :: fill_mld_epbl, fill_kd_epbl public :: fill_kd_kshear public :: fill_ice_conc, fill_ice_thick public :: remap_layer_to_z public :: remap_layer_to_sigma, remap_layer_to_zstar public :: remap_layer_to_density public :: coord_remap_proc public :: register_default_diags public :: is_canonical_diag_name public :: canonical_diag_gate_hint public :: canonical_diag_catalog_size, canonical_diag_catalog_name public :: set_diag_remap_method, parse_diag_remap_scheme public :: set_diag_mask_vanished, diag_mask_vanished_is_on integer, save :: diag_remap_method = REMAP_PPM !! Reconstruction for the conservative diagnostic vertical remap !! (`remap_column`). Set from `&ocean_diag_nml diag_remap_scheme` via !! `set_diag_remap_method`; passed by value into the device `_impl`. !! PPM default; all schemes conservative (donor-cell overlap integral). logical, save :: diag_mask_vanished = .false. !! When `.true.`, the conservative remap fills target cells that !! overlap no water (below-bottom / pinched-out in a shallow column) !! with `DIAG_MISSING_VALUE` instead of 0. Set from !! `&ocean_diag_nml mask_vanished_layers` at configure !! (`set_diag_mask_vanished`); read host-side and passed BY VALUE into !! the device `_impl`. Default `.false.` => below-bottom cells read 0 !! (bit-identical to the legacy remap). integer, parameter :: N_CANONICAL_DIAGS = 14 character(len=16), parameter :: CANONICAL_DIAG_NAMES(N_CANONICAL_DIAGS) = & [character(len=16) :: & "SSH", "temperature", "salinity", "age", "u", "v", "KE", & "MLD_EPBL", "Kd_EPBL", "Kd_KSHEAR", & "ice_conc", "ice_thick", & "pseudo_salt", "pseudo_salt_diff"] !! The single source of truth for `is_canonical_diag_name` AND the !! P7 discoverability getters (`canonical_diag_catalog_size`/ !! `_name`, wrapped by the C ABI as `rdb_ocean_canonical_*`) — !! one array, never a second hand-copied list that could drift. !! NOTE this is the catalog of NAMES `register_default_diags` may !! register, not what IS registered on a given live instance (some !! entries are gated — see `canonical_diag_gate_hint`); the live !! set is `ocean_diag_t%vars(1:nvars)%name` on an actual handle. real(wp), parameter :: VANISHED_TARGET_FLOOR = 1.0e-10_wp !! Target-cell thickness at/below which the cell is treated as !! overlapping no water (below-bottom / pinched-out). Tiny absolute !! floor: catches the exactly-zero below-bottom cells (and z*/sigma !! cells in a dry column) without masking genuinely thin overlaps. contains pure function parse_diag_remap_scheme(name) result(method) !! Map a `&ocean_diag_nml diag_remap_scheme` string to the `REMAP_*` !! enum; returns -1 for an unrecognised name (caller fails loud). character(len=*), intent(in) :: name integer :: method select case (trim(name)) case ("pcm", "PCM") method = REMAP_PCM case ("plm", "PLM") method = REMAP_PLM case ("ppm", "PPM") method = REMAP_PPM case ("ppm_h4", "PPM_H4") method = REMAP_PPM_H4 case default method = -1 end select end function parse_diag_remap_scheme subroutine set_diag_remap_method(method) !! Seed the module-level diagnostic remap reconstruction (host only). integer, intent(in) :: method diag_remap_method = method end subroutine set_diag_remap_method subroutine set_diag_mask_vanished(flag) !! Enable / disable masking of vanished (no-water) remap target cells !! to `DIAG_MISSING_VALUE` (host only). Default off. logical, intent(in) :: flag diag_mask_vanished = flag end subroutine set_diag_mask_vanished pure function diag_mask_vanished_is_on() result(on) !! Query the vanished-masking mode (used by the registration path to !! tag non-layer diagnostics with `has_missing` for the NetCDF writer). logical :: on on = diag_mask_vanished end function diag_mask_vanished_is_on ! Outer-shim + flat-impl: each fill recovers the concrete ! `ocean_state_t` via `select type` host-side, then forwards to a ! flat-impl `do concurrent` device kernel — keeps the `class(*)` ! polymorphic dispatch (blocks NVHPC device codegen) off the device. subroutine fill_ssh(state_handle, buf) !! SSH = total column thickness minus bathymetry depth, into the k=1 !! plane of `buf`. Multilayer or barotropic path per `use_multilayer`. !! Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) if (state%use_multilayer .and. allocated(state%multilayer%h_layer)) then call fill_ssh_ml_impl(state%multilayer%h_layer, state%barotropic%b, & state%multilayer%nz_ml, buf) else call fill_ssh_bt_impl(state%barotropic%h, state%barotropic%b, buf) end if end select end subroutine fill_ssh pure subroutine fill_ssh_ml_impl(h_layer, b, nz_ml, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(in) :: h_layer(:, :, :) real(wp), intent(in) :: b(:, :) ! assumed-shape-ok: diag fill — cadence-bounded integer, intent(in) :: nz_ml real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny real(wp) :: col_sum nx = size(buf, 1) ny = size(buf, 2) do concurrent(j=1:ny, i=1:nx) local(col_sum, k) col_sum = 0.0_wp do k = 1, nz_ml col_sum = col_sum + h_layer(i, j, k) end do buf(i, j, 1) = col_sum - b(i, j) end do end subroutine fill_ssh_ml_impl pure subroutine fill_ssh_bt_impl(h, b, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(in) :: h(:, :), b(:, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, nx, ny nx = size(buf, 1) ny = size(buf, 2) do concurrent(j=1:ny, i=1:nx) buf(i, j, 1) = h(i, j) - b(i, j) end do end subroutine fill_ssh_bt_impl subroutine fill_temperature(state_handle, buf) !! Layer temperature = tracers(idx_temperature)%hTr / h_layer. !! Tracer-registry indirection dereferenced HOST-side before the !! flat-impl kernel (array-of-DT deep deref blocks NVHPC device !! codegen). Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) integer :: it select type (state => state_handle) class is (ocean_state_t) it = state%multilayer%idx_temperature if (it <= 0) then call fill_zero_impl(buf) return end if call fill_tracer_impl(state%multilayer%h_layer, & state%multilayer%tracers(it)%hTr, buf) end select end subroutine fill_temperature subroutine fill_salinity(state_handle, buf) !! Layer salinity, same pattern as temperature. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) integer :: it select type (state => state_handle) class is (ocean_state_t) it = state%multilayer%idx_salinity if (it <= 0) then call fill_zero_impl(buf) return end if call fill_tracer_impl(state%multilayer%h_layer, & state%multilayer%tracers(it)%hTr, buf) end select end subroutine fill_salinity subroutine fill_age(state_handle, buf) !! Layer ideal age = tracers(idx_age)%hTr / h_layer (s). Read-out only; !! registered when `&ocean_tracers_nml enable_ideal_age` (idx_age > 0). class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) integer :: it select type (state => state_handle) class is (ocean_state_t) it = state%multilayer%idx_age if (it <= 0) then call fill_zero_impl(buf) return end if call fill_tracer_impl(state%multilayer%h_layer, & state%multilayer%tracers(it)%hTr, buf) end select end subroutine fill_age subroutine fill_pseudo_salt(state_handle, buf) !! Layer pseudo-salt = tracers(idx_pseudo_salt)%hTr / h_layer (psu). !! Read-out only; registered when `&ocean_tracers_nml !! enable_pseudo_salt` (idx_pseudo_salt > 0). Mirrors `fill_age`. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) integer :: it select type (state => state_handle) class is (ocean_state_t) it = state%multilayer%idx_pseudo_salt if (it <= 0) then call fill_zero_impl(buf) return end if call fill_tracer_impl(state%multilayer%h_layer, & state%multilayer%tracers(it)%hTr, buf) end select end subroutine fill_pseudo_salt subroutine fill_pseudo_salt_diff(state_handle, buf) !! Pseudo-salt deviation D = pseudo_salt - S (psu): a direct, !! measured proxy for how far the passive-tracer transport path !! has drifted from the active-tracer (salinity) path. Gated on !! BOTH indices being registered. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) integer :: it_ps, it_s select type (state => state_handle) class is (ocean_state_t) it_ps = state%multilayer%idx_pseudo_salt it_s = state%multilayer%idx_salinity if (it_ps <= 0 .or. it_s <= 0) then call fill_zero_impl(buf) return end if call ocean_pseudo_salt_deviation(state%multilayer%h_layer, & state%multilayer%tracers(it_ps)%hTr, & state%multilayer%tracers(it_s)%hTr, buf, & size(buf, 1), size(buf, 2), size(buf, 3), & ieee_value(0.0_wp, ieee_quiet_nan)) end select end subroutine fill_pseudo_salt_diff pure subroutine fill_tracer_impl(h_layer, hTr, buf) !! Shared flat-impl for any tracer concentration field. !! !! P7 F5 fix: a vanishing layer used to write 0.0, and 0 degC / 0 !! PSU are both LEGAL ocean values — a vanished (below-bottom or !! pinched-out) bed layer under ZSTAR_FULL therefore used to read !! back as plausible ice-point freshwater rather than as missing !! data (D3.2 of `06_python_surface_design.md`). Now: the divisor !! guard is `H_VANISHED` (the dynamic-vanish threshold, not the !! pure 1/0 armour `H_DIV_EPS` — see `rdb_constants`' D4 taxonomy), !! and the fill is IEEE NaN, matching the Python concentration !! accessor's own convention (D3.2) so an in-memory read !! (`model.diagnostic(...)`) and the NetCDF stream agree. This is !! independent of `&ocean_diag_nml mask_vanished_layers`: that knob !! only gates the conservative REMAP path's below-target-cell fill !! (`remap_column` via `register_one_canonical`'s `has_missing`); !! at the default LAYER vgrid there is no remap, so this fill is !! the only thing between "no water here" and a plausible-looking !! number, unconditionally. ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(in) :: h_layer(:, :, :) real(wp), intent(in) :: hTr(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny, nz, nz_h real(wp) :: h, qnan nx = size(buf, 1) ny = size(buf, 2) nz = size(buf, 3) nz_h = size(h_layer, 3) ! Sentinel computed once, host-side, before the do-concurrent body ! (ieee_value is a host intrinsic — see rdb_ocean_ghost_poison's ! qnan for the same pattern): pass a plain IEEE bit pattern into ! the device kernel rather than calling the intrinsic per-cell. qnan = ieee_value(0.0_wp, ieee_quiet_nan) do concurrent(k=1:min(nz, nz_h), j=1:ny, i=1:nx) h = h_layer(i, j, k) ! vanished-ok: a diagnostic substitutes the NaN missing-data ! sentinel on a vanished layer, not `rdb_vl_conc`'s copy of the ! donor's concentration (README ! consumer table); the live branch IS `rdb_vl_conc`. if (rdb_vl_is_live(h)) then buf(i, j, k) = rdb_vl_conc(hTr(i, j, k), h) else buf(i, j, k) = qnan end if end do end subroutine fill_tracer_impl subroutine fill_ice_conc(state_handle, buf) !! Total sea-ice concentration (0..1) at T-centres — the two-mode !! per-cell gather of `ice_cell_concentration_impl` (rdb_ice_state), !! inlined (see fill_ice_conc_thick_impl). Registered by !! `register_default_diags` only when `&ocean_ice_nml enable`. !! Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) if (.not. state%ice%is_init) then call fill_zero_impl(buf) return end if call fill_ice_conc_thick_impl(state%ice%ncat, & size(state%ice%m_ice, 1), & size(state%ice%m_ice, 2), & state%metrics%wet_T, state%ice%part_size, & state%ice%m_ice, .false., buf) end select end subroutine fill_ice_conc subroutine fill_ice_thick(state_handle, buf) !! Grid-mean sea-ice thickness (m) at T-centres: mice/ICE_RHO_ICE !! (MOM6 effective-thickness convention) — the two-mode per-cell !! gather of `ice_cell_concentration_impl`, inlined (see !! fill_ice_conc_thick_impl). Registered by `register_default_diags` !! only when `&ocean_ice_nml enable`. Public only for the unit-test !! suite. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) if (.not. state%ice%is_init) then call fill_zero_impl(buf) return end if call fill_ice_conc_thick_impl(state%ice%ncat, & size(state%ice%m_ice, 1), & size(state%ice%m_ice, 2), & state%metrics%wet_T, state%ice%part_size, & state%ice%m_ice, .true., buf) end select end subroutine fill_ice_thick pure subroutine fill_ice_conc_thick_impl(ncat, nx, ny, wet_T, part_size, & m_ice, emit_thick, buf) !! Shared conc/thick device kernel. Inlines the two-mode gather of !! `ice_cell_concentration_impl` (`rdb_ice_state` — convention of !! record; `test_ocean_ice_diags` pins the copies equal): ncat==1 !! legacy lumped (per-CELL m_ice, ci = 0/1), ncat>1 SIS2 ITD !! (ci = min(1, Σ part_size), mice = Σ part_size·m_ice). !! `emit_thick=.false.` ⇒ buf = ci; `.true.` ⇒ buf = mice/ICE_RHO_ICE !! (grid-mean thickness, m). Scalar flag branch is constant-folded on !! the device — one kernel, no scratch companion. !! !! Land AND ghost cells (`wet_T <= 0.5`) write the IEEE NaN !! missing-data sentinel, matching `fill_tracer_impl`'s convention — !! this used to write a plain 0.0, which is a LEGAL concentration/ !! thickness value, so `diag_field_stats`'s finite-cell mean counted !! the whole ghost ring as "0% ice" ocean and diluted the mean (e.g. !! 1200/1496 = 0.802139 on a 40x30/nghost=2 domain that is 100% !! ice-covered everywhere wet). `test_fill_ice_conc_thick_nan_ghost` !! pins the fix. integer, intent(in) :: ncat, nx, ny real(wp), intent(in) :: wet_T(nx, ny) real(wp), intent(in) :: part_size(nx, ny, 0:ncat) real(wp), intent(in) :: m_ice(nx, ny, ncat) logical, intent(in) :: emit_thick real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, c, nxl, nyl real(wp) :: mice_val, ci_val, ci_sum, qnan nxl = min(nx, size(buf, 1)) nyl = min(ny, size(buf, 2)) qnan = ieee_value(0.0_wp, ieee_quiet_nan) if (ncat == 1) then do concurrent(j=1:nyl, i=1:nxl) local(mice_val, ci_val) if (wet_T(i, j) > 0.5_wp) then if (m_ice(i, j, 1) > 0.0_wp) then mice_val = m_ice(i, j, 1) ci_val = 1.0_wp else mice_val = 0.0_wp ci_val = 0.0_wp end if buf(i, j, 1) = merge(mice_val/ICE_RHO_ICE, ci_val, emit_thick) else buf(i, j, 1) = qnan end if end do else do concurrent(j=1:nyl, i=1:nxl) local(c, mice_val, ci_val, ci_sum) if (wet_T(i, j) > 0.5_wp) then mice_val = 0.0_wp ci_sum = 0.0_wp do c = 1, ncat mice_val = mice_val + part_size(i, j, c)*m_ice(i, j, c) ci_sum = ci_sum + part_size(i, j, c) end do ci_val = min(1.0_wp, ci_sum) buf(i, j, 1) = merge(mice_val/ICE_RHO_ICE, ci_val, emit_thick) else buf(i, j, 1) = qnan end if end do end if end subroutine fill_ice_conc_thick_impl pure subroutine fill_zero_impl(buf) !! Device-side zero of `buf` — used when a fill's input is missing !! (e.g. tracer not registered) so the fold reads a defined value. ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(inout) :: buf(:, :, :) integer :: i, j, k, nx, ny, nz nx = size(buf, 1) ny = size(buf, 2) nz = size(buf, 3) do concurrent(k=1:nz, j=1:ny, i=1:nx) buf(i, j, k) = 0.0_wp end do end subroutine fill_zero_impl subroutine fill_u_centre(state_handle, buf) !! C-grid u-face → cell centre by simple 2-point average. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_u_centre_impl(state%multilayer%u_face_x_layer, buf) end select end subroutine fill_u_centre pure subroutine fill_u_centre_impl(u_face, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded); ! face-sized u_face(:,:,:) has nx+1 first dim, incompatible with cell-sized buf. real(wp), intent(in) :: u_face(:, :, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny, nz nx = min(size(buf, 1), size(u_face, 1) - 1) ny = min(size(buf, 2), size(u_face, 2)) nz = min(size(buf, 3), size(u_face, 3)) do concurrent(k=1:nz, j=1:ny, i=1:nx) buf(i, j, k) = 0.5_wp*(u_face(i, j, k) + u_face(i + 1, j, k)) end do end subroutine fill_u_centre_impl subroutine fill_v_centre(state_handle, buf) class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_v_centre_impl(state%multilayer%v_face_y_layer, buf) end select end subroutine fill_v_centre pure subroutine fill_v_centre_impl(v_face, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded); ! face-sized v_face(:,:,:) has ny+1 second dim, incompatible with cell-sized buf. real(wp), intent(in) :: v_face(:, :, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny, nz nx = min(size(buf, 1), size(v_face, 1)) ny = min(size(buf, 2), size(v_face, 2) - 1) nz = min(size(buf, 3), size(v_face, 3)) do concurrent(k=1:nz, j=1:ny, i=1:nx) buf(i, j, k) = 0.5_wp*(v_face(i, j, k) + v_face(i, j + 1, k)) end do end subroutine fill_v_centre_impl subroutine fill_ke(state_handle, buf) !! KE per cell on the C-grid: KE = 0.25·(u_W² + u_E² + v_S² + v_N²), !! the discrete C-grid KE-density (consistent with the Coriolis !! KE_ARAKAWA stencil). Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_ke_impl(state%multilayer%u_face_x_layer, & state%multilayer%v_face_y_layer, buf) end select end subroutine fill_ke pure subroutine fill_ke_impl(u_face, v_face, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded); ! face-sized arrays have nx+1/ny+1 first/second dims, incompatible with buf. real(wp), intent(in) :: u_face(:, :, :), v_face(:, :, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny, nz nx = min(size(buf, 1), size(u_face, 1) - 1, size(v_face, 1)) ny = min(size(buf, 2), size(u_face, 2), size(v_face, 2) - 1) nz = min(size(buf, 3), size(u_face, 3), size(v_face, 3)) do concurrent(k=1:nz, j=1:ny, i=1:nx) buf(i, j, k) = 0.25_wp*( & u_face(i, j, k)*u_face(i, j, k) + & u_face(i + 1, j, k)*u_face(i + 1, j, k) + & v_face(i, j, k)*v_face(i, j, k) + & v_face(i, j + 1, k)*v_face(i, j + 1, k)) end do end subroutine fill_ke_impl subroutine remap_layer_to_z(state_handle, z_out, layer_buf, output_buf, is_extensive) !! Layer→fixed-z vertical remap, CONSERVATIVE (donor-cell overlap via !! `remap_column`, scheme `diag_remap_method`). `is_extensive=.false.` !! ⇒ INTENSIVE (thickness-weighted average of overlapping source layers); !! `.true.` ⇒ EXTENSIVE (thickness-integrated field — column integral !! redistributed across targets, Σ preserved when the z-grid spans H). !! `z_out(:)` = target INTERFACE depths (m, positive-down, shallow→deep, !! implicit 0 surface); output cell m spans `[z_out(m-1), z_out(m)]`. !! Thicknesses clipped to column total H = Σ h_layer over the LIVE !! layers (exact conservation; a vanished layer carries zero weight — !! see `remap_layer_to_vcoord_impl`); below-seafloor cells read 0. !! k=1 bed, k=nz surface. !! Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(in) :: z_out(:) real(wp), intent(in) :: layer_buf(:, :, :) real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive select type (state => state_handle) class is (ocean_state_t) call remap_layer_to_vcoord_impl(DIAG_VGRID_Z_FIXED, state%multilayer%h_layer, & z_out, layer_buf, output_buf, is_extensive, & diag_remap_method, diag_mask_vanished, & DIAG_MISSING_VALUE) end select end subroutine remap_layer_to_z subroutine remap_layer_to_sigma(state_handle, levels, layer_buf, output_buf, is_extensive) !! Layer→fixed-sigma vertical remap (terrain-following output grid). !! `levels(:)` are cumulative sigma fractions (0..1, shallow→deep); the !! m-th output cell spans `[levels(m-1), levels(m)] * col_h`. Same !! conservative donor-cell overlap as `remap_layer_to_z`; see its !! docstring for the intensive/extensive contract. class(*), intent(in) :: state_handle real(wp), intent(in) :: levels(:) real(wp), intent(in) :: layer_buf(:, :, :) real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive select type (state => state_handle) class is (ocean_state_t) call remap_layer_to_vcoord_impl(DIAG_VGRID_SIGMA, state%multilayer%h_layer, & levels, layer_buf, output_buf, is_extensive, & diag_remap_method, diag_mask_vanished, & DIAG_MISSING_VALUE) end select end subroutine remap_layer_to_sigma subroutine remap_layer_to_zstar(state_handle, levels, layer_buf, output_buf, is_extensive) !! Layer→fixed-z* vertical remap (SSH-tracking stretched-depth output !! grid). `levels(:)` are reference interface depths (m, positive-down, !! shallow→deep); the deepest is the reference total depth H_ref and the !! per-column grid is stretched by `col_h / H_ref`. Identical to !! `remap_layer_to_sigma` under uniform levels — supply a non-uniform !! (fine-near-surface) reference for it to differ. Same conservative !! donor-cell overlap; see `remap_layer_to_z` for the intensive/extensive !! contract. class(*), intent(in) :: state_handle real(wp), intent(in) :: levels(:) real(wp), intent(in) :: layer_buf(:, :, :) real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive select type (state => state_handle) class is (ocean_state_t) call remap_layer_to_vcoord_impl(DIAG_VGRID_ZSTAR, state%multilayer%h_layer, & levels, layer_buf, output_buf, is_extensive, & diag_remap_method, diag_mask_vanished, & DIAG_MISSING_VALUE) end select end subroutine remap_layer_to_zstar subroutine coord_remap_proc(coord, remap) !! Return the default remap procedure pointer for an output vgrid. !! Used by the registration path to attach the right conservative !! remap when a diagnostic selects a non-layer output coordinate. !! Null for LAYER / unknown (no remap needed). integer, intent(in) :: coord procedure(diag_remap_proc), pointer, intent(out) :: remap select case (coord) case (DIAG_VGRID_Z_FIXED) remap => remap_layer_to_z case (DIAG_VGRID_SIGMA) remap => remap_layer_to_sigma case (DIAG_VGRID_ZSTAR) remap => remap_layer_to_zstar case (DIAG_VGRID_DENSITY) remap => remap_layer_to_density case default remap => null() end select end subroutine coord_remap_proc pure subroutine remap_layer_to_vcoord_impl(coord_type, h_layer, levels, layer_buf, & output_buf, is_extensive, method, & mask_vanished, missing) !! Per-column conservative layer→output-coordinate remap as a !! `do concurrent` over (j, i). Builds the source column TOP-DOWN !! (work index 1 = surface = state k=nz) and the target cells from the !! `levels` interface positions (implicit 0 surface), clips both to the !! column total, then runs the donor-cell overlap integral !! (`remap_column`, scheme `method`) on a common `n = max(nz, nz_out)` !! padded partition. Intensive remaps the value directly; extensive !! divides in / multiplies out by thickness so the column integral !! redistributes (sum preserved). !! !! `coord_type` selects how `levels(m)` maps to a target interface !! depth (a per-column scale hoisted out of the inner loop): !! * `Z_FIXED`: `levels` are absolute depths (m, positive-down) — !! scale 1 (the bit-identical legacy fixed-z path). !! * `SIGMA`: `levels` are cumulative fractions (0..1) — depth = !! `levels(m) * col_h` (terrain-following). !! * `ZSTAR`: `levels` are reference depths (deepest = H_ref) — !! depth = `levels(m) * col_h / H_ref` (SSH-tracking: when !! col_h == H_ref the grid is the reference grid). Identical to !! SIGMA under uniform levels; differs only with a non-uniform !! (e.g. fine-near-surface) reference. !! !! **Vanished source layers carry ZERO WEIGHT, not a value.** A !! source layer that is not live (`rdb_vl_is_live`) enters the !! overlap integral with `dz = 0` and `q = 0`, and its thickness is !! left out of the column total the targets are clipped to. Its !! `layer_buf` value is never read: for a concentration it is the !! NaN missing-data sentinel (`fill_tracer_impl`), which the !! donor-cell reconstruction would otherwise smear into every target !! cell of the column; for content it is `h·c_live` by I1′, a copy of !! the donor layer the remap already counts. A column with !! no vanished layer is bit-identical. !! !! Fixed-size `NZ_STACK_MAX` stack locals via `local(...)` — automatic !! arrays sized from a dummy crash NVHPC stdpar device codegen. ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded); ! size() used to derive loop bounds from the actual buffer dimensions. integer, intent(in) :: coord_type real(wp), intent(in) :: h_layer(:, :, :) real(wp), intent(in) :: levels(:) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(in) :: layer_buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive integer, intent(in) :: method logical, intent(in) :: mask_vanished real(wp), intent(in) :: missing integer :: i, j, k, m, nx, ny, nz, nz_out, n real(wp) :: dz_old(NZ_STACK_MAX), dz_new(NZ_STACK_MAX) real(wp) :: q_old(NZ_STACK_MAX), q_new(NZ_STACK_MAX) real(wp) :: col_h, zf_prev, zf, dz, lvl_scale, href nx = size(layer_buf, 1) ny = size(layer_buf, 2) nz = size(layer_buf, 3) nz_out = size(levels) n = max(nz, nz_out) href = levels(nz_out) ! ZSTAR reference total depth (deepest interface) do concurrent(j=1:ny, i=1:nx) & local(dz_old, dz_new, q_old, q_new, k, m, col_h, zf_prev, zf, dz, lvl_scale) ! --- source column TOP-DOWN: work index k = state index nz-k+1 --- ! A vanished layer gets zero weight (see the docstring). col_h = 0.0_wp do k = 1, nz dz_old(k) = h_layer(i, j, nz - k + 1) if (rdb_vl_is_live(dz_old(k))) then q_old(k) = layer_buf(i, j, nz - k + 1) else dz_old(k) = 0.0_wp q_old(k) = 0.0_wp end if col_h = col_h + dz_old(k) end do if (is_extensive) then do k = 1, nz if (dz_old(k) > 1.0e-12_wp) then q_old(k) = q_old(k)/dz_old(k) ! integral -> concentration else q_old(k) = 0.0_wp end if end do end if ! pad the source to n with zero-thickness layers at the deep end do k = nz + 1, n dz_old(k) = 0.0_wp q_old(k) = 0.0_wp end do ! --- per-column level->depth scale (loop-invariant, hoisted) --- select case (coord_type) case (DIAG_VGRID_SIGMA) lvl_scale = col_h case (DIAG_VGRID_ZSTAR) if (href > 1.0e-12_wp) then lvl_scale = col_h/href else lvl_scale = 1.0_wp end if case default ! DIAG_VGRID_Z_FIXED: levels are absolute depths lvl_scale = 1.0_wp end select ! --- target cells from scaled interfaces (implicit 0 surface), ! clipped to the column total H so the totals match exactly --- zf_prev = 0.0_wp do m = 1, nz_out zf = min(max(levels(m)*lvl_scale, 0.0_wp), col_h) dz = zf - zf_prev if (dz < 0.0_wp) dz = 0.0_wp dz_new(m) = dz zf_prev = zf end do do m = nz_out + 1, n dz_new(m) = 0.0_wp end do call remap_column(method, n, dz_old, dz_new, q_old, q_new) do m = 1, nz_out if (mask_vanished .and. dz_new(m) <= VANISHED_TARGET_FLOOR) then ! Target cell overlaps no water (below-bottom / pinched-out): ! emit the missing sentinel instead of a misleading 0. output_buf(i, j, m) = missing else if (is_extensive) then output_buf(i, j, m) = q_new(m)*dz_new(m) else output_buf(i, j, m) = q_new(m) end if end do end do end subroutine remap_layer_to_vcoord_impl subroutine remap_layer_to_density(state_handle, z_out, layer_buf, output_buf, is_extensive) !! Conservative layer→DENSITY-space remap (`DIAG_VGRID_DENSITY`). !! `z_out(:)` = monotone-increasing target potential DENSITIES (kg/m³). !! Per column: layer potential density via device EOS at the diag !! reference pressure, invert the profile to target-interface depths !! (`invert_density_targets`), then remap. Lightest target → surface. !! `is_extensive=.false.` ⇒ INTENSIVE (weighted average); `.true.` ⇒ !! EXTENSIVE (column integral redistributed across bins, Σ preserved). !! Public only for the unit-test suite. class(*), intent(in) :: state_handle real(wp), intent(in) :: z_out(:) real(wp), intent(in) :: layer_buf(:, :, :) real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive integer :: it, is_ real(wp) :: pref select type (state => state_handle) class is (ocean_state_t) it = state%multilayer%idx_temperature is_ = state%multilayer%idx_salinity pref = state%vcoord%rho_ref_pressure if (it <= 0 .or. is_ <= 0) then call fill_zero_impl(output_buf) return end if call remap_layer_to_density_impl(state%multilayer%h_layer, & state%multilayer%tracers(it)%hTr, & state%multilayer%tracers(is_)%hTr, & state%eos, pref, z_out, & layer_buf, output_buf, is_extensive, & diag_remap_method) end select end subroutine remap_layer_to_density pure subroutine remap_layer_to_density_impl(h_layer, hT, hS, eos, rho_ref_p, & rho_tgt, layer_buf, output_buf, & is_extensive, method) !! Per-column density-space remap, `do concurrent` over (j, i). !! Source column TOP-DOWN + layer potential density (EOS at !! `rho_ref_p`), invert profile to interface depths !! (`invert_density_targets`), then donor-cell remap. Cells outside !! the column density range read 0. Intensive/extensive as the z-remap. !! !! **Vanished source layers** carry zero weight exactly as in !! `remap_layer_to_vcoord_impl` (`dz = 0`, `q = 0`, value never read), !! and take the density of the nearest LIVE layer (the one above, !! else the first one below) rather than an EOS evaluation of the !! filler: the EOS substitutes reference T/S on a vanished layer, and !! the PPM edge between a live layer and a zero-thickness neighbour IS !! that neighbour's density (`invert_density_targets`), so a `rho_0` !! filler would kink the profile the targets are inverted against. !! A column with no vanished layer is bit-identical; a column with no !! live layer keeps the legacy `eos(0, 0)` fill. ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded); ! size() used to derive loop bounds from the actual buffer dimensions. real(wp), intent(in) :: h_layer(:, :, :) real(wp), intent(in) :: hT(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(in) :: hS(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded type(eos_t), intent(in) :: eos real(wp), intent(in) :: rho_ref_p real(wp), intent(in) :: rho_tgt(:) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(in) :: layer_buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded real(wp), intent(inout) :: output_buf(:, :, :) logical, intent(in) :: is_extensive integer, intent(in) :: method integer :: i, j, k, m, nx, ny, nz, n_bin, n real(wp) :: dz_old(NZ_STACK_MAX), dz_new(NZ_STACK_MAX) real(wp) :: q_old(NZ_STACK_MAX), q_new(NZ_STACK_MAX) ! z_iface holds n_bin+2 entries (`invert_density_targets` writes ! z_new(1:n_int+2) with n_int = n_bin, and :798 reads z_iface(n_bin+2)), ! so it needs NZ_STACK_MAX+2 — at NZ_STACK_MAX+1 a density diagnostic ! with n_rho_out == NZ_STACK_MAX (which the output-level guard permits) ! wrote and read one element past the end. real(wp) :: rhoc(NZ_STACK_MAX), z_iface(NZ_STACK_MAX + 2) real(wp) :: rho_tgt_c(NZ_STACK_MAX) real(wp) :: hh, tt, ss, rho_live logical :: seen_live nx = size(layer_buf, 1) ny = size(layer_buf, 2) nz = size(layer_buf, 3) n_bin = size(rho_tgt) ! number of density bins (output cells) n = max(nz, n_bin) do concurrent(j=1:ny, i=1:nx) & local(dz_old, dz_new, q_old, q_new, rhoc, z_iface, rho_tgt_c, & k, m, hh, tt, ss, rho_live, seen_live) ! --- source column TOP-DOWN + layer potential density --- ! Vanished layers: zero weight, density of the nearest live layer ! (see the docstring). seen_live = .false. rho_live = 0.0_wp do k = 1, nz hh = h_layer(i, j, nz - k + 1) if (rdb_vl_is_live(hh)) then dz_old(k) = hh q_old(k) = layer_buf(i, j, nz - k + 1) tt = rdb_vl_conc(hT(i, j, nz - k + 1), hh) ss = rdb_vl_conc(hS(i, j, nz - k + 1), hh) rhoc(k) = eos_density_point(eos, tt, ss, rho_ref_p) if (.not. seen_live) then ! Back-fill the vanished run above the first live layer. do m = 1, k - 1 rhoc(m) = rhoc(k) end do end if seen_live = .true. rho_live = rhoc(k) else dz_old(k) = 0.0_wp q_old(k) = 0.0_wp if (seen_live) then rhoc(k) = rho_live else rhoc(k) = eos_density_point(eos, 0.0_wp, 0.0_wp, rho_ref_p) end if end if end do if (is_extensive) then do k = 1, nz if (dz_old(k) > 1.0e-12_wp) then q_old(k) = q_old(k)/dz_old(k) else q_old(k) = 0.0_wp end if end do end if do k = nz + 1, n dz_old(k) = 0.0_wp q_old(k) = 0.0_wp end do ! --- invert instantaneous density to target-interface depths --- ! n_bin output cells; the LAST cell absorbs to the bed so ! Σ dz_new == H exactly (extensive conservation, densest mass kept). ! Copy assumed-shape rho_tgt(:) into a contiguous fixed-size local ! first: passing the assumed-shape actual to the explicit-shape ! dummy makes flang emit host copy helpers absent in the AMD device ! runtime → offload link fails. Element reads off the descriptor OK. do m = 1, n_bin rho_tgt_c(m) = rho_tgt(m) end do call invert_density_targets(nz, dz_old, rhoc, n_bin, rho_tgt_c, z_iface) do m = 1, n_bin - 1 dz_new(m) = z_iface(m + 1) - z_iface(m) if (dz_new(m) < 0.0_wp) dz_new(m) = 0.0_wp end do dz_new(n_bin) = z_iface(n_bin + 2) - z_iface(n_bin) if (dz_new(n_bin) < 0.0_wp) dz_new(n_bin) = 0.0_wp do m = n_bin + 1, n dz_new(m) = 0.0_wp end do call remap_column(method, n, dz_old, dz_new, q_old, q_new) do m = 1, n_bin if (is_extensive) then output_buf(i, j, m) = q_new(m)*dz_new(m) else output_buf(i, j, m) = q_new(m) end if end do end do end subroutine remap_layer_to_density_impl subroutine fill_mld_epbl(state_handle, buf) !! EPBL active-mixing-layer depth (m) into the k=1 plane. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_2d_impl(state%epbl%mld, buf) end select end subroutine fill_mld_epbl pure subroutine fill_2d_impl(field, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(in) :: field(:, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, nx, ny nx = size(buf, 1) ny = size(buf, 2) do concurrent(j=1:ny, i=1:nx) buf(i, j, 1) = field(i, j) end do end subroutine fill_2d_impl subroutine fill_kd_epbl(state_handle, buf) !! EPBL interface diffusivity (m^2/s). Buffer is layer-shaped !! (nx, ny, nz); we emit the value at the BOTTOM interface of !! each layer (kd_int(:, :, k) convention), losing only the !! identically-zero surface interface. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_kd_epbl_impl(state%epbl%kd_int, buf) end select end subroutine fill_kd_epbl subroutine fill_kd_kshear(state_handle, buf) !! Kappa-shear interface diffusivity (m^2/s). Buffer is !! layer-shaped (nx, ny, nz); we emit the value at the BOTTOM !! interface of each layer (kd_int(:, :, k) convention), losing !! only the identically-zero surface interface. class(*), intent(in) :: state_handle real(wp), intent(inout) :: buf(:, :, :) select type (state => state_handle) class is (ocean_state_t) call fill_kd_epbl_impl(state%kshear%kd_int, buf) end select end subroutine fill_kd_kshear pure subroutine fill_kd_epbl_impl(kd_int, buf) ! assumed-shape-ok: diag fill — fires once per output frame (cadence-bounded). real(wp), intent(in) :: kd_int(:, :, :) real(wp), intent(inout) :: buf(:, :, :) ! assumed-shape-ok: diag fill — cadence-bounded integer :: i, j, k, nx, ny, nz nx = size(buf, 1) ny = size(buf, 2) nz = size(buf, 3) do concurrent(k=1:nz, j=1:ny, i=1:nx) buf(i, j, k) = kd_int(i, j, k) end do end subroutine fill_kd_epbl_impl subroutine register_default_diags(state, dt_out, specs, default_coord) !! Register the canonical ocean diagnostic variable set. Called once !! from setup (driver init or test) after `state%diag%init`. When !! `specs` (parsed `&ocean_diag_nml diags`) is present, each canonical !! diagnostic consults it: a `:off` entry skips registration entirely, !! and `:cadence` / `:op` / `:coord` attributes override the defaults. !! `default_coord` (the global `&ocean_diag_nml vgrid`, default LAYER) !! sets the output vgrid for layered diagnostics absent a per-diag !! `:coord`. Absent `specs` + LAYER default => the canonical defaults, !! bit-identical to the legacy behaviour. The canonical set also !! gains `ice_conc` / `ice_thick` when `&ocean_ice_nml enable`. type(ocean_state_t), intent(inout), target :: state real(wp), intent(in), optional :: dt_out type(diag_spec_t), intent(in), optional :: specs(:) integer, intent(in), optional :: default_coord integer :: nz, dcoord real(wp) :: dtout dtout = 3600.0_wp if (present(dt_out)) dtout = dt_out dcoord = DIAG_VGRID_LAYER if (present(default_coord)) dcoord = default_coord nz = state%multilayer%nz_ml call register_one_canonical(state, specs, dcoord, dtout, "SSH", "m", fill_ssh, & 1, DIAG_OP_INSTANT, "sea_surface_height_above_geoid", & "sea_surface_height") if (state%multilayer%idx_temperature > 0) then call register_one_canonical(state, specs, dcoord, dtout, "temperature", "degC", & fill_temperature, nz, DIAG_OP_MEAN, & "sea_water_potential_temperature", & "sea_water_potential_temperature") end if if (state%multilayer%idx_salinity > 0) then call register_one_canonical(state, specs, dcoord, dtout, "salinity", "psu", & fill_salinity, nz, DIAG_OP_MEAN, & "sea_water_salinity", "sea_water_salinity") end if if (state%multilayer%idx_age > 0) then call register_one_canonical(state, specs, dcoord, dtout, "age", "s", fill_age, & nz, DIAG_OP_MEAN, "ideal_age_of_sea_water", & "age_of_sea_water") end if if (state%multilayer%idx_pseudo_salt > 0) then call register_one_canonical(state, specs, dcoord, dtout, "pseudo_salt", "psu", & fill_pseudo_salt, nz, DIAG_OP_MEAN, & "pseudo_salt_passive_tracer") call register_one_canonical(state, specs, dcoord, dtout, "pseudo_salt_diff", "psu", & fill_pseudo_salt_diff, nz, DIAG_OP_MEAN, & "difference_between_pseudo_salt_and_salt") end if call register_one_canonical(state, specs, dcoord, dtout, "u", "m s-1", fill_u_centre, & nz, DIAG_OP_MEAN, "eastward_velocity_at_cell_centre") call register_one_canonical(state, specs, dcoord, dtout, "v", "m s-1", fill_v_centre, & nz, DIAG_OP_MEAN, "northward_velocity_at_cell_centre") call register_one_canonical(state, specs, dcoord, dtout, "KE", "m2 s-2", fill_ke, & nz, DIAG_OP_MEAN, "kinetic_energy_per_unit_mass") if (state%epbl%enable) then call register_one_canonical(state, specs, dcoord, dtout, "MLD_EPBL", "m", & fill_mld_epbl, 1, DIAG_OP_MEAN, & "epbl_active_mixing_layer_depth") call register_one_canonical(state, specs, dcoord, dtout, "Kd_EPBL", "m2 s-1", & fill_kd_epbl, nz, DIAG_OP_MEAN, & "epbl_diffusivity_at_layer_bottom_interface") end if if (state%kshear%enable) then call register_one_canonical(state, specs, dcoord, dtout, "Kd_KSHEAR", "m2 s-1", & fill_kd_kshear, nz, DIAG_OP_MEAN, & "kappa_shear_diffusivity_at_layer_bottom_interface") end if if (state%ice%enable) then call register_one_canonical(state, specs, dcoord, dtout, "ice_conc", "1", & fill_ice_conc, 1, DIAG_OP_MEAN, & "sea_ice_area_fraction", "sea_ice_area_fraction") ! ice_thick carries NO CF standard_name: the CF `sea_ice_thickness` ! conventionally means actual floe thickness, but this is the ! grid-mean (effective) thickness mice/ICE_RHO_ICE — so only the ! long_name identifies it (omit standard_name, as u/v/KE do). call register_one_canonical(state, specs, dcoord, dtout, "ice_thick", "m", & fill_ice_thick, 1, DIAG_OP_MEAN, & "grid_mean_sea_ice_thickness") end if end subroutine register_default_diags subroutine register_one_canonical(state, specs, default_coord, dtout, name, units, & fill, n3, def_op, long_name, standard_name) !! Register one canonical diagnostic, applying its `specs` entry: !! skip if `:off`; override cadence / time-op; pick the output vgrid !! (`:coord` override, else `default_coord`, else LAYER) and attach the !! matching conservative remap. 2D diagnostics (`n3 <= 1`) ignore any !! coord request (no vertical to remap). All canonical diagnostics are !! INTENSIVE (thickness-weighted average on remap). type(ocean_state_t), intent(inout), target :: state type(diag_spec_t), intent(in), optional :: specs(:) integer, intent(in) :: default_coord, n3, def_op real(wp), intent(in) :: dtout character(len=*), intent(in) :: name, units, long_name character(len=*), intent(in), optional :: standard_name procedure(diag_fill_proc) :: fill integer :: nx, ny, op, coord real(wp) :: dto logical :: skip procedure(diag_remap_proc), pointer :: remap op = def_op dto = dtout coord = default_coord call resolve_canonical_spec(specs, name, op, dto, coord, skip) if (skip) return ! 2D fields have no vertical axis to remap onto. if (n3 <= 1) coord = DIAG_VGRID_LAYER call coord_remap_proc(coord, remap) nx = size(state%barotropic%h, 1) ny = size(state%barotropic%h, 2) if (associated(remap)) then call state%diag%register(name, units=units, fill=fill, n1=nx, n2=ny, n3=n3, & long_name=long_name, standard_name=standard_name, & time_op=op, dt_out=dto, output_vgrid=coord, & remap=remap, is_extensive=.false., & has_missing=(diag_mask_vanished .and. & coord /= DIAG_VGRID_DENSITY)) else call state%diag%register(name, units=units, fill=fill, n1=nx, n2=ny, n3=n3, & long_name=long_name, standard_name=standard_name, & time_op=op, dt_out=dto) end if end subroutine register_one_canonical pure subroutine resolve_canonical_spec(specs, name, time_op, dt_out, coord, skip) !! Look up canonical diagnostic `name` in the parsed `specs` and apply !! its overrides: `skip=.true.` if the entry is `:off`; otherwise !! `time_op` / `dt_out` / `coord` are overwritten when the entry sets !! them. No matching entry (or absent `specs`) leaves the caller's !! defaults untouched. type(diag_spec_t), intent(in), optional :: specs(:) character(len=*), intent(in) :: name integer, intent(inout) :: time_op real(wp), intent(inout) :: dt_out integer, intent(inout) :: coord logical, intent(out) :: skip integer :: i skip = .false. if (.not. present(specs)) return do i = 1, size(specs) if (trim(specs(i)%name) == trim(name)) then if (specs(i)%off) then skip = .true. return end if if (specs(i)%time_op /= DIAG_OP_UNSET) time_op = specs(i)%time_op if (specs(i)%dt_out > 0.0_wp) dt_out = specs(i)%dt_out if (specs(i)%coord /= DIAG_COORD_UNSET) coord = specs(i)%coord return end if end do end subroutine resolve_canonical_spec pure function is_canonical_diag_name(name) result(yes) !! `.true.` if `name` is one of the canonical default diagnostics !! registered by `register_default_diags` (including the !! conditionally-registered EPBL / kappa-shear / tracer / sea-ice !! diags). Used by the selection orchestrator to route a spec entry !! to either the canonical-override path or the derived-catalog !! registration path. character(len=*), intent(in) :: name logical :: yes integer :: i yes = .false. do i = 1, N_CANONICAL_DIAGS if (trim(CANONICAL_DIAG_NAMES(i)) == trim(name)) then yes = .true. return end if end do end function is_canonical_diag_name pure function canonical_diag_catalog_size() result(n) !! Number of names in the canonical-diagnostic catalog (the static !! set `register_default_diags` MAY register — some entries are !! gated, see `canonical_diag_gate_hint`). Bounds for !! `canonical_diag_catalog_name`'s index argument. Public for the !! P7 discoverability C ABI (`rdb_ocean_canonical_catalog_size`) !! and the unit-test suite. integer :: n n = N_CANONICAL_DIAGS end function canonical_diag_catalog_size pure function canonical_diag_catalog_name(i) result(name) !! Name of canonical-catalog entry `i` (1-based). Public for the P7 !! discoverability C ABI and the unit-test suite. integer, intent(in) :: i character(len=16) :: name name = CANONICAL_DIAG_NAMES(i) end function canonical_diag_catalog_name pure function canonical_diag_gate_hint(name) result(hint) !! The namelist gate whose closure suppresses canonical diagnostic !! `name`, for the "you asked for this and did not get it" warning. !! `""` for the four unconditional diagnostics (SSH/u/v/KE) — a hint !! of `""` means "this one should have registered; that is a bug, !! not a config". MUST be kept in lock-step with !! `register_default_diags`'s gates; the count of non-empty hints !! must equal the number of conditionally-registered canonical !! diagnostics (`test_ocean_diag/diag_gate_hint_covers_every_gate` is !! the lock — any PR that adds a new gated canonical diagnostic must !! add its hint here or that test fails). This duplicates the gate !! NAMES only, never the gate LOGIC — the single `if` in !! `register_default_diags` remains the one place the gate is !! evaluated. character(len=*), intent(in) :: name character(len=64) :: hint select case (trim(name)) case ("temperature", "salinity") hint = "&ocean_thermo_nml enable_thermodynamics" case ("age") hint = "&ocean_tracers_nml enable_ideal_age" case ("MLD_EPBL", "Kd_EPBL") hint = "&ocean_epbl_nml enable" case ("Kd_KSHEAR") hint = "&ocean_kappa_shear_nml enable" case ("ice_conc", "ice_thick") hint = "&ocean_ice_nml enable" case default hint = "" end select end function canonical_diag_gate_hint #include "rdb_vanished_layer.inc" end module rdb_ocean_diag_fills