Second half of one outer step: derive Q_heat/Q_salt from
the surface-flux component set (no-op unless
&ocean_forcing_nml enable_components) and, when diagnostics
are configured, run one ocean_diag_t%step. Call immediately
after engine_step and, when sea ice is enabled,
engine_step_ice too — see engine_step’s docstring for why
this is a separate call.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(ocean_engine_t), | intent(inout) | :: | engine | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| real(kind=wp), | intent(in) | :: | t |
Simulation time at the START of the step this finalizes
(the same |
||
| integer, | intent(out), | optional | :: | ierr |
subroutine engine_step_finalize(engine, dt, t, ierr) !! Second half of one outer step: derive `Q_heat`/`Q_salt` from !! the surface-flux component set (no-op unless !! `&ocean_forcing_nml enable_components`) and, when diagnostics !! are configured, run one `ocean_diag_t%step`. Call immediately !! after `engine_step` and, when sea ice is enabled, !! `engine_step_ice` too — see `engine_step`'s docstring for why !! this is a separate call. type(ocean_engine_t), intent(inout) :: engine real(wp), intent(in) :: dt real(wp), intent(in) :: t !! Simulation time at the START of the step this finalizes !! (the same `t` passed to the matching `engine_step` call) — !! `t + dt` is what reaches the diag manager, matching !! `driver_run_ocean`'s post-advance `t_current`. integer, intent(out), optional :: ierr if (present(ierr)) ierr = OCEAN_STATUS_OK ! Ice-shelf basal melt (P2b): solve the three-equation interface on ! every covered column and fill the OWNED heat_cavity/salt_cavity ! components. MUST precede the assembler, which folds them into ! Q_heat/Q_salt. Same thermo cadence, and a no-op (immediate ! return) when &ocean_cavity_melt_nml enable=.false. Cavity x sea ! ice is refused at configure, so the ordering against ! engine_step_ice's fillers is not a live question. call ocean_cavity_flux_step(engine%grid, engine%state%cavity_flux, & engine%state%metrics, engine%state%multilayer, & engine%state%eos, engine%state%surface_flux, & active=engine%state%dyn%enable_thermodynamics & .and. engine%state%dyn%is_thermo_step()) ! Phase 4b — the MELT-ONLY fallback for the boundary-layer `u_*`. ! ! When `&ocean_tdrag_nml` is on, the RK2 stage drivers publish ! `ss%stress_shelf` from the top drag's own `stress_top`, in-stage ! and unlagged, and this branch stays out of the way. When it is ! OFF but basal melt is on, nothing else would give KPP/EPBL an ! under-ice `u_*` at all — they would mix a covered column on the ! masked (exactly zero) wind stress. The melt slot already solved ! for a friction velocity with the SAME `C_d` (the one-drag- ! coefficient rule: `&ocean_cavity_melt_nml cdrag_top` must equal ! `&ocean_tdrag_nml cd`), so re-deriving `|tau_top| = rho_0*u_*^2` ! from it is the consistent answer, not a second drag law. ! ! HONEST LIMIT: `cav%ustar` is refreshed at the THERMO cadence, at ! the END of the outer step, so this path reaches the boundary-layer ! schemes ONE OUTER STEP LATE. The top-drag path has no such lag. ! `cav%ustar` is exactly 0 on every uncovered column, so the ! published field keeps `stress_shelf`'s "zero off the cover" ! invariant. ! ! A CALL, not an inline `do concurrent`, and the reason is a GPU ! rule rather than a style preference. A `do concurrent` written ! here would reference `engine%state%surface_stress%stress_shelf`, ! i.e. it would walk the ENGINE, and `ocean_engine_t` is not a ! mapped object -- only `engine%state` is. nvfortran then emits a ! data clause for the whole `engine` and aborts at run time with ! "variable in data clause is partially present on the device: ! name=engine". Measured, cc70, 2026-09-20: it did exactly that. ! Host-dereferencing the two component arrays AT the call site and ! handing them to a flat explicit-shape kernel is the fix -- the ! outer-shim + flat-impl pattern. ! ! CLAUDE.md's "write a host-gated pass INLINE" rule does not apply: ! that rule exists because an escaping state array pessimises the ! OTHER `do concurrent` loops in the calling routine, and ! `engine_step_finalize` has none -- it is a four-call orchestrator. ! ! Placeholder safety is STRUCTURAL, not a runtime branch: the gate ! is `cavity_flux%enable`, and the melt slot allocates `ustar` at ! `(nx, ny)` exactly when that is set (`(1,1)` otherwise), while ! `stress_shelf` is always full size. So the explicit-shape dummies ! below can only ever be reached with matching, full-size actuals. if (engine%state%cavity_flux%enable .and. .not. engine%state%tdrag%enable) then call ocean_surface_stress_set_shelf_from_ustar( & engine%state%surface_stress%stress_shelf, & engine%state%cavity_flux%ustar, & engine%state%surface_stress%rho0, & size(engine%state%surface_stress%stress_shelf, 1), & size(engine%state%surface_stress%stress_shelf, 2)) end if ! Ice-shelf cover: the assembler is where the atmospheric bands and ! the cavity's own heat_cavity/salt_cavity are still separable, so ! it is where `1 - cover_frac` is applied — and applying it there ! (rather than at apply time) is also what makes the `Q_heat` / ! `Q_salt` that KPP and EPBL read for `B_0` the MASKED values. ! Cavity off ⇒ the original call, byte-identical. if (engine%state%metrics%use_cavity) then call ocean_surface_flux_assemble(engine%grid, engine%state%surface_flux, & engine%state%multilayer, & active=engine%state%dyn%enable_thermodynamics & .and. engine%state%dyn%is_thermo_step(), & cover_frac=engine%state%metrics%cover_frac) else call ocean_surface_flux_assemble(engine%grid, engine%state%surface_flux, & engine%state%multilayer, & active=engine%state%dyn%enable_thermodynamics & .and. engine%state%dyn%is_thermo_step()) end if if (engine%diag_enabled) then call engine%state%diag%step(engine%state, dt, t + dt) end if end subroutine engine_step_finalize