Additive correction that redistributes the penetrating
shortwave fraction of Q_heat through the upper water column
as a two-band exponential (Paulson & Simpson 1977; Jerlov
types), instead of leaving all of it deposited at the surface
layer by ocean_surface_flux_apply_tracers.
Penetrating irradiance at downward depth d:
I(d) = I0 · [ R·exp(-d/zeta1) + (1-R)·exp(-d/zeta2) ]
with I0 = sw_pen_frac · Q_heat(i,j). Per-layer absorbed SW =
I(d_top) - I(d_bot); the bed (k = k_bot, the first LIVE layer
counting up — 1 off z_fixed) is treated as opaque
(I_bot := 0) so the column absorbs all of I0 and energy is
conserved exactly (Σ_k absorbed_k = I0).
Additive-correction structure: the surface kernel already
deposited the full I0 lump at k = nz; this kernel removes it
there (-inv_scale · I0) and adds the distributed profile, so
the net column heat change versus the legacy all-at-nz
deposition is zero — shortwave only MOVES heat in depth. Both
the hTr and heat_budget_surface increments are mirrored, so
the total surface heat budget is unchanged (just depth-spread).
Bottom-up convention (k = nz surface, k = 1 bed). No-op
unless sf%has_sw .and. sf%has_heat, a temperature tracer is
registered, and (optionally) active is true. Default-off
(sw_pen_frac = 0 ⇒ has_sw = .false.) leaves the path
byte-for-byte unchanged.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(hgrid_t), | intent(in) | :: | grid | |||
| type(ocean_surface_flux_t), | intent(in), | optional | :: | sf |
Optional — absent ⇒ no-op (no forcing configured). |
|
| type(multilayer_state_t), | intent(inout) | :: | ms | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| logical, | intent(in), | optional | :: | active |
Optional thermo-cadence gate. Present-and-false ⇒ early return; absent ⇒ kernel runs. |
|
| real(kind=wp), | intent(in), | optional | :: | cover_frac(:,:) |
Optional ice-shelf cover fraction ( |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | private | :: | idx_T | ||||
| logical, | private | :: | masked | ||||
| integer, | private | :: | nx | ||||
| integer, | private | :: | ny | ||||
| integer, | private | :: | nz |
subroutine ocean_surface_flux_apply_sw_penetration(grid, sf, ms, dt, active, cover_frac) !! Additive correction that redistributes the penetrating !! shortwave fraction of `Q_heat` through the upper water column !! as a two-band exponential (Paulson & Simpson 1977; Jerlov !! types), instead of leaving all of it deposited at the surface !! layer by `ocean_surface_flux_apply_tracers`. !! !! Penetrating irradiance at downward depth `d`: !! I(d) = I0 · [ R·exp(-d/zeta1) + (1-R)·exp(-d/zeta2) ] !! with `I0 = sw_pen_frac · Q_heat(i,j)`. Per-layer absorbed SW = !! `I(d_top) - I(d_bot)`; the bed (`k = k_bot`, the first LIVE layer !! counting up — `1` off `z_fixed`) is treated as opaque !! (`I_bot := 0`) so the column absorbs all of `I0` and energy is !! conserved exactly (Σ_k absorbed_k = I0). !! !! Additive-correction structure: the surface kernel already !! deposited the full `I0` lump at `k = nz`; this kernel removes it !! there (`-inv_scale · I0`) and adds the distributed profile, so !! the net column heat change versus the legacy all-at-`nz` !! deposition is zero — shortwave only MOVES heat in depth. Both !! the `hTr` and `heat_budget_surface` increments are mirrored, so !! the total surface heat budget is unchanged (just depth-spread). !! !! Bottom-up convention (`k = nz` surface, `k = 1` bed). No-op !! unless `sf%has_sw .and. sf%has_heat`, a temperature tracer is !! registered, and (optionally) `active` is true. Default-off !! (`sw_pen_frac = 0` ⇒ `has_sw = .false.`) leaves the path !! byte-for-byte unchanged. type(hgrid_t), intent(in) :: grid type(ocean_surface_flux_t), intent(in), optional :: sf !! Optional — absent ⇒ no-op (no forcing configured). type(multilayer_state_t), intent(inout) :: ms real(wp), intent(in) :: dt logical, intent(in), optional :: active !! Optional thermo-cadence gate. Present-and-false ⇒ early !! return; absent ⇒ kernel runs. real(wp), intent(in), optional :: cover_frac(:, :) !! Optional ice-shelf cover fraction (`metrics%cover_frac`, !! v1 binary). Present ⇒ the penetrating irradiance is scaled !! by `1 - cover_frac`, so a covered column absorbs NOTHING — !! no sunlight reaches the ocean through several hundred metres !! of ice. Needed even on the `sw_source="net_heat"` branch, !! where `Q_heat` is already assembler-masked: this kernel's !! job is to MOVE a surface lump down the column, and on a !! masked column the lump it would remove was never deposited !! (the same argument `&ocean_wetdry_nml` makes for a dry !! column). Absent ⇒ the original kernel, byte-identical. ! assumed-shape-ok: thermo-cadence shim, forwarded to an ! explicit-shape `_impl` before the device loop. integer :: nx, ny, nz, idx_T logical :: masked if (present(active)) then if (.not. active) return end if if (.not. present(sf)) return if (.not. (sf%has_sw .and. sf%has_heat)) return if (.not. allocated(ms%tracers)) return idx_T = ms%idx_temperature if (idx_T <= 0) return nx = grid%nx_total ny = grid%ny_total nz = ms%nz_ml ! Shim+_impl split: keep the `tracers(idx_T)%hTr` registry deref on ! the host (array-of-DT indirection blocks NVHPC device codegen). ! The irradiance source is selected HOST-SIDE (`sw_from_qsw`): the ! PR-12 `q_sw` component is allocated only under `use_components`, ! so passing it as an actual argument is only legal on the branch ! guarded by the host flag (validate_config forces ! enable_components when sw_source="q_sw", making this total). The ! additive-correction identity `-I0 + Σ_k I0·(T_top - T_bot) = 0` ! holds for ANY I0, so the source swap cannot break conservation. masked = .false. if (present(cover_frac)) then masked = (size(cover_frac, 1) == nx .and. size(cover_frac, 2) == ny) end if if (masked) then if (sf%sw_from_qsw) then call apply_sw_penetration_cover_impl(ms%tracers(idx_T)%hTr, & ms%heat_budget_surface, & ms%h_layer, ms%k_bot, ms%wet_mask, cover_frac, sf%q_sw, & dt/(sf%rho0*sf%cp), sf%sw_pen_frac, & sf%sw_band_ratio, sf%sw_zeta1, sf%sw_zeta2, & nz, nx, ny) else call apply_sw_penetration_cover_impl(ms%tracers(idx_T)%hTr, & ms%heat_budget_surface, & ms%h_layer, ms%k_bot, ms%wet_mask, cover_frac, sf%Q_heat, & dt/(sf%rho0*sf%cp), sf%sw_pen_frac, & sf%sw_band_ratio, sf%sw_zeta1, sf%sw_zeta2, & nz, nx, ny) end if return end if if (sf%sw_from_qsw) then call apply_sw_penetration_impl(ms%tracers(idx_T)%hTr, & ms%heat_budget_surface, & ms%h_layer, ms%k_bot, ms%wet_mask, sf%q_sw, & dt/(sf%rho0*sf%cp), sf%sw_pen_frac, & sf%sw_band_ratio, sf%sw_zeta1, sf%sw_zeta2, & nz, nx, ny) else call apply_sw_penetration_impl(ms%tracers(idx_T)%hTr, & ms%heat_budget_surface, & ms%h_layer, ms%k_bot, ms%wet_mask, sf%Q_heat, & dt/(sf%rho0*sf%cp), sf%sw_pen_frac, & sf%sw_band_ratio, sf%sw_zeta1, sf%sw_zeta2, & nz, nx, ny) end if end subroutine ocean_surface_flux_apply_sw_penetration