Make the surface-stress pair valid in every ghost cell, then
re-derive stress_mag from it.
Why this exists. tau_x/tau_y are C-grid face fields that
several kernels read ONE CELL BEYOND the cell they write:
ocean_surfstress_derived_impl averages tau_x(i)+tau_x(i+1)
into the cell-centred stress_mag, which feeds KPP/EPBL u_*.mle_face_ustar_x/y (Fox-Kemper / Bodner) take a 4-point
corner average reaching tau_y(i-1, ·) / tau_x(·, j-1).At an MPI seam those reads land in ghost cells that belong to the neighbour rank, so they MUST come from an exchange. Nothing may extrapolate them: a zero-gradient / edge-copy fill silently substitutes this rank’s edge value for the neighbour’s real data, which is decomposition-dependent and therefore invisible to any single-rank test. (This mirrors the convention in MOM6, which halo-exchanges the stress pair and never extrapolates forcing.)
Order is load-bearing and matches the prognostic-state path in
ocean_dyn_step_split: exchange, THEN periodic wrap on any axis
the exchange did not own, THEN the north fold. The periodic
kernels are skipped on a decomposed axis because the halo already
filled those ghosts — running both would overwrite correct
neighbour data with a local wrap (see rdb_ocean_periodic).
stress_mag needs no wrap or fold of its own: it is recomputed
LAST, over the full array, from a tau pair whose ghosts are by
then already valid, so its ghosts come out right for free.
Safe to call before ocean_state_enter_data with
device_resident = .false. (the configure-time seed path).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(ocean_surface_stress_t), | intent(inout) | :: | ss |
Stress slot whose |
||
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(ocean_bc_state_t), | intent(in) | :: | bc |
Supplies |
||
| logical, | intent(in), | optional | :: | device_resident |
Forwarded to the halo primitives; |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | private | :: | ng | ||||
| integer, | private | :: | nxp | ||||
| integer, | private | :: | nxt | ||||
| integer, | private | :: | nyp | ||||
| integer, | private | :: | nyt | ||||
| logical, | private | :: | wrap_x | ||||
| logical, | private | :: | wrap_y |
subroutine ocean_seam_refresh_surface_stress(ss, grid, bc, device_resident) !! Make the surface-stress pair valid in every ghost cell, then !! re-derive `stress_mag` from it. !! !! **Why this exists.** `tau_x`/`tau_y` are C-grid face fields that !! several kernels read ONE CELL BEYOND the cell they write: !! !! * `ocean_surfstress_derived_impl` averages `tau_x(i)`+`tau_x(i+1)` !! into the cell-centred `stress_mag`, which feeds KPP/EPBL `u_*`. !! * `mle_face_ustar_x/y` (Fox-Kemper / Bodner) take a 4-point !! corner average reaching `tau_y(i-1, ·)` / `tau_x(·, j-1)`. !! !! At an MPI seam those reads land in ghost cells that belong to the !! neighbour rank, so they MUST come from an exchange. Nothing may !! extrapolate them: a zero-gradient / edge-copy fill silently !! substitutes this rank's edge value for the neighbour's real data, !! which is decomposition-dependent and therefore invisible to any !! single-rank test. (This mirrors the convention in MOM6, which !! halo-exchanges the stress pair and never extrapolates forcing.) !! !! **Order is load-bearing** and matches the prognostic-state path in !! `ocean_dyn_step_split`: exchange, THEN periodic wrap on any axis !! the exchange did not own, THEN the north fold. The periodic !! kernels are skipped on a decomposed axis because the halo already !! filled those ghosts — running both would overwrite correct !! neighbour data with a local wrap (see `rdb_ocean_periodic`). !! !! `stress_mag` needs no wrap or fold of its own: it is recomputed !! LAST, over the full array, from a `tau` pair whose ghosts are by !! then already valid, so its ghosts come out right for free. !! !! Safe to call before `ocean_state_enter_data` with !! `device_resident = .false.` (the configure-time seed path). type(ocean_surface_stress_t), intent(inout) :: ss !! Stress slot whose `tau_x`/`tau_y` ghosts are to be filled and !! whose `stress_mag` is then refreshed. type(hgrid_t), intent(in) :: grid type(ocean_bc_state_t), intent(in) :: bc !! Supplies `periodic_x`/`periodic_y`/`north_fold`. logical, intent(in), optional :: device_resident !! Forwarded to the halo primitives; `.false.` for host-side !! configure-time calls. integer :: nxt, nyt, nxp, nyp, ng logical :: wrap_x, wrap_y if (.not. allocated(ss%tau_x) .or. .not. allocated(ss%tau_y)) return nxt = grid%nx_total nyt = grid%ny_total nxp = grid%nx_phys nyp = grid%ny_phys ng = grid%nghost call profiler_start("ocean_comms_stress") call oh_count_suppress_on() call ocean_halo_face_x(ss%tau_x, device_resident) call ocean_halo_face_y(ss%tau_y, device_resident) call oh_count_suppress_off() call profiler_stop("ocean_comms_stress") ! Local periodic wrap only on an axis the halo did NOT own. wrap_x = bc%periodic_x .and. (.not. ocean_halo_is_decomposed_x()) wrap_y = bc%periodic_y .and. (.not. ocean_halo_is_decomposed_y()) if (wrap_x .or. wrap_y) then call ocean_periodic_wrap_face_x_2d(ss%tau_x, nxt + 1, nyt, & nxp, nyp, ng, wrap_x, wrap_y) call ocean_periodic_wrap_face_y_2d(ss%tau_y, nxt, nyt + 1, & nxp, nyp, ng, wrap_x, wrap_y) end if ! Tripolar north fold. `tau_x`/`tau_y` are TRUE VECTOR components, ! so the sign-flipping u/v-face variants are the correct ones (the ! scalar-copy duplicates in `rdb_ocean_metrics` exist precisely ! because those are NOT vectors). px = 1: the local kernels; px > 1: ! one owner-routed exchange group (`ocean_fold_wrap_stress`). if (bc%north_fold) call ocean_fold_wrap_stress(grid, bc, ss%tau_x, ss%tau_y, device_resident) call ocean_surface_stress_set_derived(grid, ss) end subroutine ocean_seam_refresh_surface_stress