Skip to content

sbc: rotate initial wind/stress coefficients on rotated grids#919

Open
koldunovn wants to merge 1 commit into
mainfrom
fix-coldstart-wind-rotation
Open

sbc: rotate initial wind/stress coefficients on rotated grids#919
koldunovn wants to merge 1 commit into
mainfrom
fix-coldstart-wind-rotation

Conversation

@koldunovn
Copy link
Copy Markdown
Member

Summary

On a rotated grid (rotated_grid = .true., e.g. the standard CORE2 setup), the initial surface-forcing wind/stress interpolation coefficients are never rotated into the rotated-grid frame. As a result, for the first forcing window of a run the wind stress applied to both the ocean and the sea ice stays in the geographic frame and is mis-directed by the local geo→rotated (g2r) angle.

Mechanism

The g2r rotation of the wind/stress is applied only in sbc_do (src/gen_surface_forcing.F90), and only when the time-interpolation coefficients are refreshed:

! sbc_do: do_rotation_wind is set ONLY inside the getcoeffld (refresh) branch
if ( ((rdate > nc_time(t_indx_p1)) .and. ...) .or. force_newcoeff) then
   call getcoeffld(fld_idx, rdate, partit, mesh)
   if ((l_xwind .and. (fld_idx==i_xwind)) .and. rotated_grid) do_rotation_wind=.true.
endif
...
if (do_rotation_wind) then        ! rotate coef_a/coef_b for wind
   ... call vector_g2r(coef_a(i_xwind,i), coef_a(i_ywind,i), ...) ...
end if

The cold-start initialization nc_sbc_ini loads the very first set of coefficients but never rotates them:

! nc_sbc_ini
do fld_idx = 1, i_totfl
   call getcoeffld(fld_idx, rdate, partit, mesh)   ! load first coefficients
end do
call data_timeinterp(rdate, partit)                ! build atmdata -> NO rotation

At cold start, force_newcoeff is .false. (no year change) and rdate lies inside the first forcing interval, so the refresh condition in sbc_do is not met on the first steps → do_rotation_wind stays .false. → the rotation block is skipped → the wind used (atmdata = rdate*coef_a + coef_b) is the unrotated geographic wind. The rotation only kicks in once the run crosses the first forcing-window boundary and sbc_do calls getcoeffld again.

Impact

  • Transient: it self-corrects after the first forcing-window crossing (for 3-hourly JRA55 at dt = 1800 s, ≈ 6 steps / ≈ 3 model hours).
  • Magnitude-preserving: a rotation is orthogonal, so |stress| is unchanged. This is why the bug is invisible to every magnitude-based diagnostic — u*, turbulent fluxes, SST/SSS/SSH, ice area/volume, ice/ocean speed ratios — and to multi-year climate validation. Only the wind-stress direction is wrong, so it perturbs the initial ice drift and surface-current direction for the first few steps.
  • Scope: only rotated grids (rotated_grid = .true.). Non-rotated setups are unaffected.

How it was found

Discovered while bit-comparing a C re-implementation of the FESOM2 forcing/EVP path against this Fortran code at ocean step 1 (CORE2 mesh, JRA55, dt=1800, cold start). Every EVP input was bit-identical except the wind-on-ice stress stress_atmice, which differed at all surface nodes. The difference was a pure rotation: per-node |stress| matched to 1.0000, while the direction differed by exactly the local g2r vector angle (stress_C = g2r(stress_Fortran), residual 0.0 at every node). Tracing it showed the C code rotated the wind from step 1 while this Fortran code did not, isolating the missing rotation to nc_sbc_ini.

Fix

In nc_sbc_ini, after loading the initial coefficients, apply the same g2r rotation that sbc_do already applies on refresh (guarded by l_xwind/l_xstre and rotated_grid). It is a faithful mirror of the existing sbc_do rotation block, so steady-state behaviour is unchanged; it only fixes the cold-start window.

Validation

Compared step-1 wind-on-ice stress (CORE2, JRA55, dt=1800, cold start) against a rotated-frame reference, before vs after the fix:

quantity before fix after fix
stress_atmice C-vs-ref, per node pure rotation: ` ·
resulting first-EVP-subcycle ice velocity ~70% relative diff ~4e-16

Multi-year climate is unchanged (the bug was a bounded transient that self-corrects after the first forcing window), confirming the fix only touches the initial-window forcing direction.

On a rotated grid (rotated_grid=.true.), the geo->rotated (g2r) rotation of the
wind/stress interpolation coefficients is applied only in sbc_do, gated on
do_rotation_wind which is set only when coefficients are refreshed (inside the
getcoeffld branch). The cold-start initialization nc_sbc_ini loads the first
coefficients (getcoeffld) and builds the wind (data_timeinterp) but never
rotates them. At step 1, force_newcoeff is .false. (no year change) and rdate
lies inside the first forcing interval, so sbc_do does not refresh -> the
rotation is skipped -> the first forcing window's wind stress on BOTH ocean and
ice stays in the geographic frame, mis-directed by the local g2r angle. It
self-corrects once the run crosses the first forcing-window boundary.

The error is magnitude-preserving (rotation is orthogonal), so it is invisible
to |stress|-based diagnostics (u*, turbulent fluxes, SST/SSS/SSH, ice
area/volume, ice/ocean speed ratios) and to multi-year climate; only the
wind-stress direction is wrong, at cold start.

Fix: rotate the initial coefficients in nc_sbc_ini, mirroring the existing
sbc_do rotation (guarded by l_xwind/l_xstre .and. rotated_grid). Steady-state
behaviour is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants