P2.4b: sea-ice per-step physics — ocean-side frazil
accumulation, EVP dynamics (every outer step, independent of
the thermo cadence: the ice’s own fast/slow split) plus the
resulting ice->ocean stress coupling, and, at thermo cadence
(engine%state%dyn%is_thermo_step()), category transport
followed by the atmospheric-forcing / basal-flux /
frazil-uptake / column-thermo / snowfall / brine / heat /
shortwave / ITD chain. Transcribed VERBATIM from
driver_run_ocean’s former inline sea-ice block — the
internal call order is load-bearing (see the “MANDATED ORDER”
comment below, itself carried over unchanged) and is NOT
reordered here. Returns immediately, a no-op, when
&ocean_ice_nml enable = .false. — bit-identical to before
this phase.
Call between engine_step and engine_step_finalize — the
surface-flux assembler the finalize call runs reads the
salt/heat components this routine writes (the driver’s own
“PR-12 … MUST sit here” comment), so it must run AFTER this
returns. cfg is threaded through (unlike engine_step)
because the thermo-forcing knobs this block reads every
thermo-cadence step (air_temp/restore_lambda/sw_down/
snowfall/transport/adv_substeps/roll_factor) are read
straight from &ocean_ice_nml in the driver too, not cached
at setup (unlike evp_params/ic_par, which the config
builds once, up front).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(ocean_engine_t), | intent(inout) | :: | engine | |||
| type(config_t), | intent(in) | :: | cfg | |||
| real(kind=wp), | intent(in) | :: | dt | |||
| real(kind=wp), | intent(in) | :: | t |
Unused today (the sea-ice block has no direct time
dependence — only |
||
| integer, | intent(out), | optional | :: | ierr |
Present for signature symmetry; no failure path in this
routine sets it today — |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | private | :: | ice_n_trunc | ||||
| logical, | private | :: | ice_ok | ||||
| integer(kind=int64), | private | :: | n_trunc_glob | ||||
| integer(kind=int64), | private | :: | n_trunc_loc |
subroutine engine_step_ice(engine, cfg, dt, t, ierr) !! P2.4b: sea-ice per-step physics — ocean-side frazil !! accumulation, EVP dynamics (every outer step, independent of !! the thermo cadence: the ice's own fast/slow split) plus the !! resulting ice->ocean stress coupling, and, at thermo cadence !! (`engine%state%dyn%is_thermo_step()`), category transport !! followed by the atmospheric-forcing / basal-flux / !! frazil-uptake / column-thermo / snowfall / brine / heat / !! shortwave / ITD chain. Transcribed VERBATIM from !! `driver_run_ocean`'s former inline sea-ice block — the !! internal call order is load-bearing (see the "MANDATED ORDER" !! comment below, itself carried over unchanged) and is NOT !! reordered here. Returns immediately, a no-op, when !! `&ocean_ice_nml enable = .false.` — bit-identical to before !! this phase. !! !! Call between `engine_step` and `engine_step_finalize` — the !! surface-flux assembler the finalize call runs reads the !! salt/heat components this routine writes (the driver's own !! "PR-12 ... MUST sit here" comment), so it must run AFTER this !! returns. `cfg` is threaded through (unlike `engine_step`) !! because the thermo-forcing knobs this block reads every !! thermo-cadence step (`air_temp`/`restore_lambda`/`sw_down`/ !! `snowfall`/`transport`/`adv_substeps`/`roll_factor`) are read !! straight from `&ocean_ice_nml` in the driver too, not cached !! at setup (unlike `evp_params`/`ic_par`, which the config !! builds once, up front). type(ocean_engine_t), intent(inout) :: engine type(config_t), intent(in) :: cfg real(wp), intent(in) :: dt real(wp), intent(in) :: t !! Unused today (the sea-ice block has no direct time !! dependence — only `dt` and the `therm_dt`/thermo-cadence !! machinery derived from it); present for signature symmetry !! with `engine_step`/`engine_step_finalize`. integer, intent(out), optional :: ierr !! Present for signature symmetry; no failure path in this !! routine sets it today — `ice_transport_step`'s !! conservation/positivity check still `error stop`s, !! matching the pre-existing driver behaviour verbatim. logical :: ice_ok integer :: ice_n_trunc integer(int64) :: n_trunc_loc, n_trunc_glob if (present(ierr)) ierr = OCEAN_STATUS_OK if (.not. engine%state%ice%enable) return ! Sea-ice PR 1/3b/3c: ocean-side frazil accumulation, then (at thermo ! cadence) the atmospheric-forcing seam + basal flux + column ! thermodynamics + frazil-bank spend, mediated back to the ocean via ! the salt/heat couplers. ice_frazil_accumulate runs on the FINAL ! (post-RK2-average, post-ALE-remap) tracer state of the outer step — ! clamping inside the RK2 stages would not bound the averaged result — ! so T(k=nz) >= T_f(S) holds at every step boundary the ice model ! observes. call profiler_start("ice_frazil") call ice_frazil_accumulate(engine%grid, engine%state%eos, & engine%state%multilayer, & engine%state%ice%frazil_heat, & engine%state%ice%heat_budget_frazil) call profiler_stop("ice_frazil") ! Sea-ice PR 5: C-grid EVP dynamics, every outer step (NOT ! thermo-cadence gated — EVP is the ice's own fast/slow split, ! analogous to the ocean's barotropic/baroclinic split, and runs ! every outer step regardless of the thermo cadence). ice_evp_step ! writes ice%u_ice/v_ice directly (replacing the PR-4b transport ! sampler, gated off below); ice_ocean_stress_flux immediately ! mediates the resulting drag into the ocean's surface-stress field so ! the NEXT ocean momentum step feels it (one-step-lagged, same ! convention as the frazil/heat couplers). if (engine%state%ice%dynamics) then call profiler_start("ice_evp") ! PR 36: the CFL bound must use the dt TRANSPORT will actually ! consume, not this call's outer `dt` -- EVP runs every outer ! step, transport at thermo cadence (`engine%state%dyn%therm_dt(dt)`, ! the same bound function the transport call below already uses). call ice_evp_step(engine%grid, engine%state%metrics, & engine%state%coriolis_adv%f_corner, & engine%state%ice, engine%state%multilayer, dt, & engine%evp_params, engine%state%bc, & dt_transport=engine%state%dyn%therm_dt(dt), & n_trunc=ice_n_trunc) ! The truncation count is per tile; the warning reports the whole ! domain. Exact integer sum, and only when the clip is on (the ! count is identically 0 otherwise), so the default costs no ! collective. Gated on the RUN's decomposition, not the job's ! rank count (a serial reference inside a multi-rank job). if (engine%evp_params%cfl_trunc > 0.0_wp .and. ocean_halo_is_decomposed()) then n_trunc_loc = int(ice_n_trunc, int64) call halo_allreduce_sum_i8(n_trunc_loc, n_trunc_glob) ice_n_trunc = int(n_trunc_glob) end if if (ice_n_trunc > 0 .and. engine%decomp%rx == 0 .and. engine%decomp%ry == 0) then call logger%warning("ice_evp_step: ice velocity CFL-truncated at "// & to_string(ice_n_trunc)//" faces "// & "(&ocean_ice_nml cfl_trunc); the ice dynamics is "// & "unstable or the transport step is too long") end if ! X5 rides inside: the blended tau pair is seam-refreshed ! (exchange -> wrap -> fold) before `stress_mag` and the restart ! mirror are taken from it. call ice_ocean_stress_flux(engine%state%metrics, engine%state%surface_stress, & engine%state%ice, engine%grid, engine%state%bc) call profiler_stop("ice_evp") end if ! Sea-ice PR 3b/3c: at thermo cadence, drive the column live and spend ! the bank. Post-step, outer_step_count has already incremented, so ! is_thermo_step() fires at the END of each thermo window — the rates ! written here are integrated by exactly ONE apply (therm_dt) in the ! NEXT window (MEKE/frazil one-step-lag convention). Gated on ! enable_thermodynamics too: with thermo off the apply path never ! fires, and spending the bank / stepping the column would strand ! un-mediated salt/heat. ! ! MANDATED ORDER (PLAN_ICE_PR3c "Driver wiring" + the ordering caution ! therein; PR 4a appends the ITD restore; PR 26 inserts the snowfall ! ocean-share contributor; PR 31 inserts the shortwave coupler): ! forcing -> basal -> frazil uptake -> column driver -> snowfall ocean ! share -> brine coupler -> heat coupler -> shortwave coupler -> ! adjust categories (ITD restore; ncat=1 short-circuits inside). ! Frazil uptake MUST precede the column driver: `ice_frazil_uptake_impl` ! OVERWRITES `salt_flux_diag` (unconditional zero, then a gated write), ! so if the column ran first its net-melt salt contribution would be ! clobbered. Running frazil first (fresh window) then having the ! column driver ADD its net-melt term on top composes correctly — a ! cell with no frazil but melting ice starts at 0 and the column adds ! its (negative) contribution. `ice_snowfall_ocean_share` (PR 26, ! gated on `has_snowfall`) MUST run AFTER the column driver (which ! unconditionally zeroes `heat_flux_diag` — running snowfall first ! would have its contribution clobbered) and BEFORE the brine/heat ! couplers (which overwrite `Q_salt`/`Q_heat` from ! `salt_flux_diag`/`heat_flux_diag` — running it after would never ! reach the ocean). It ADDS to both diags, same contract as the ! column driver's net-melt term. The couplers run next, after every ! contributor has written salt_flux_diag/heat_flux_diag for this ! window. `ice_ocean_sw_flux` (PR 31) runs immediately AFTER ! `ice_ocean_heat_flux`: in the components-off default it ADDS ! `sw_thru_diag` onto the `Q_heat` the heat coupler just ! full-overwrote, so it must observe that overwrite first (the ! shortwave is deliberately NOT in heat_flux_diag — see ! `ice_ocean_sw_flux`, no double count in either component mode). ! `ice_adjust_categories` runs LAST: it only reshuffles category ! area/mass/enthalpy/salt (no thermodynamics, no diag writes) after ! every thermodynamic thickness change this window, so the couplers ! (which only READ the per-cell diags written above) are unaffected ! by it running after them. if (engine%state%dyn%enable_thermodynamics .and. & engine%state%dyn%is_thermo_step()) then ! Sea-ice PR 4b: category ice/snow transport + compress_ice, ! BEFORE the thermo forcing chain (SIS2 slow sequence: ! dynamics+transport, then slow thermo). Cadence = the thermo step ! (the ice slow step); gated on `&ocean_ice_nml transport` (default ! off => byte-identical). if (cfg%ocean%ice%transport) then call profiler_start("ice_transport") call ice_transport_step(engine%grid, engine%state%metrics, & engine%state%multilayer, & engine%state%ice, engine%state%dyn%therm_dt(dt), & cfg%ocean%ice%adv_substeps, cfg%ocean%ice%roll_factor, & ice_ok, bc=engine%state%bc) call profiler_stop("ice_transport") if (.not. ice_ok) then call logger%error("ice_transport_step: conservation/positivity "// & "violation (negative mass, orphan snow, or a "// & "compress-time consistency failure)") error stop "ice_transport_step: conservation/positivity violation" end if end if call profiler_start("ice_thermo") call ice_atm_forcing_restoring(engine%state%ice, & cfg%ocean%ice%air_temp, & cfg%ocean%ice%restore_lambda, & cfg%ocean%ice%sw_down, & cfg%ocean%ice%snowfall) call ice_compute_basal_flux(engine%grid, engine%state%eos, & engine%state%multilayer, engine%state%ice, & engine%state%dyn%therm_dt(dt)) call ice_frazil_uptake(engine%grid, engine%state%eos, engine%state%multilayer, & engine%state%ice, engine%state%dyn%therm_dt(dt)) call ice_thermo_driver_step(engine%grid, engine%state%eos, & engine%state%multilayer, engine%state%ice, & engine%state%dyn%therm_dt(dt)) if (engine%state%ice%has_snowfall) then call ice_snowfall_ocean_share(engine%grid, engine%state%ice, & engine%state%dyn%therm_dt(dt)) end if ! Every contributor above wrote the per-cell flux diags on ! PHYSICAL cells; the couplers below copy the FULL array into the ! ocean's surface fluxes, whose seam ghosts the ocean reads. call ocean_halo_exchange_ice_fluxes(engine%state%ice, engine%grid, engine%state%bc) call ice_ocean_brine_flux(engine%state%surface_flux, engine%state%ice) call ice_ocean_heat_flux(engine%state%surface_flux, engine%state%ice) call ice_ocean_sw_flux(engine%state%surface_flux, engine%state%ice) call ice_adjust_categories(engine%grid, engine%state%multilayer, engine%state%ice) call profiler_stop("ice_thermo") ! X1: the transport compress, the column thermodynamics and the ! ITD restore above all write PHYSICAL cells only, and the ! category state changes nowhere else — so this one exchange per ! thermo window is what keeps the ghosts the next EVP gather and ! stress blend read equal to the neighbour's (or, on one rank, to ! the periodic partner's) owned cells. call ocean_halo_exchange_ice_state(engine%state%ice, engine%grid, engine%state%bc) end if end subroutine engine_step_ice