Retain XLONG_U/XLAT_V staggered edge in lon_m/lat_m - #3892
Merged
asalmgren merged 1 commit intoAug 21, 2026
Merged
Conversation
- Grow typed subdomain so intersection copy keeps staggered edge - Clamp lon_m/lat_m ghosts to var_fab extent, not cell domain - Leave sinPhi_m/cosPhi_m on cell clamp; Coriolis reads their ghosts - Document the staggering GetOceanToAtmosCornerCoordinates promises
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR addresses https://github.com/erf-model/remora-erf-driver/issues/11
Specific changes / choices
Motivation Summary
XLONG_UandXLAT_Vare edge-staggered in awrfinputfile —west_east_stagandsouth_north_stag, sonx+1andny+1entries respectively. The fabs that receive them carry CELL index type, so the typed subdomain built atERF_InitFromWRFInput.cpp:488is a cell box, and the intersection copy that follows,Box subdomain_to_fill_typed(convert(subdomain_tmp, var_fab_from_file.box().ixType())); ... Box intersection = var_fab.box() & var_fab_from_file.box();silently drops the final staggered column of
XLONG_Uand the final staggered row ofXLAT_V. The ghost fill further down then clamps against the cell domain, solon_m(nx,j)receives a duplicate of columnnx-1rather than the true eastern edge, andlat_m(i,ny)likewise.Measured on a real
wrfinput_d01withwest_east = 117,west_east_stag = 118:XLONG_U(0,50,117)in the file-85.09668lon_m(117,50)before this change-85.38144(duplicate of column 116)0.285°— a full grid spacingXLAT_Vshows the same at the north edge (45.51647in the file against45.27907).Why the outermost column matters
lon_mandlat_mare not only diagnostic. They are the coordinate source a coupled ocean model consumes as a cell-corner mesh, throughERF::GetOceanToAtmosCornerCoordinates. The consumer reconstructs true corners by averaging along the axis where each array is cell-centred and taking it as-is along the axis where it is already nodal:That requires
lonout toi = nx. With the duplicate in place, the easternmost column of corner quads collapses to zero width, its polygon area is zero, and every conservative-remap weight built against it vanishes — so the outermost column of the domain silently exchanges nothing. The north row fails the same way throughlat_m.The fix
Grow the typed subdomain by one in the staggered direction before the intersection copy, guarded so it can only ever widen into data the file actually contains:
and clamp the
lon_m/lat_mghost fill againstvar_fab's own extent rather than the cell domain, so the retained column is what the ghost receives.var_fabkeeps CELL index type; only its extent widens.Note the apparently obvious fix — widening the clamp alone — is a no-op. Before this change
var_fab.box().bigEnd(0) == i_hi == 116, sovar_fabnever contained column 117 and clamping against its own extent changed nothing. The subdomain has to grow first.Why this approach, against established practice
This is worth stating because the alternative is what the reference implementation does, and the choice is not obvious.
SCRIP requires explicitly enumerated corners: a grid file carries
grid_corner_lat(grid_size, grid_corners)andgrid_corner_lon(...), four per cell for quads, boundary cells included (SCRIP 1.4 user guide; Jones, Mon. Wea. Rev. 127, 2204–2210, 1999, https://doi.org/10.1175/1520-0493(1999)127%3C2204:FASOCR%3E2.0.CO;2).COAWST's SCRIP-file generator never reads the staggered arrays.
Lib/SCRIP_COAWST/read_wrf.freads onlyXLONG/XLAT— mass points — andcreate_psimeshaverages four surrounding mass points into psi corners, yielding(nx-1, ny-1), i.e. interior corners only.create_fullgrid.fthen reaches the boundary by linear extrapolation:one ring out on all four sides, then the four corners from the already-extrapolated edges.
So the comparison is:
ERF's approach is the better-conditioned one provided the column survives the read — WRF genuinely stores the face positions, so the boundary ring is data rather than an approximation. This change is what stops that advantage being discarded. A COAWST-style extrapolated ring does exist in the driver behind
driver.remap_extend_corner_ring, defaulting off, so nothing was masking the defect.MOM6 offers the closest structural precedent for handling the same problem:
extrapolate_metricextends the supergrid latitude (var(i,j) = 2.0*var(i,j+1) - var(i,j+2)) under a sentinel guard, applied before subsampling togeoLatT/Bu/Cu/Cv, with the Coriolis parameter recomputed from the result over the full data domain.sinPhi_m/cosPhi_mare deliberately left on the old clamplat_mis dual-purpose, and its two consumers want different values in that final row. The remap wants the true northern edge latitude; the Coriolis factors want the latitude at the point they are differencing to.sinPhi_mandcosPhi_mtherefore keep the cell-domain clamp, and this change is bit-identical for every existing ERF consumer.That is not merely conservative — it is the position the surrounding ecosystem takes:
finto the outermost face.ERF_MakeMomSources.cpp:377-391reads0.5*(sphi_arr(i,j,0) + sphi_arr(i-1,j,0))for x-momentum,0.5*(sphi_arr(i,j,0) + sphi_arr(i,j-1,0))for y, andcphi_arr(i,j,0)directly for z — overmfi.nodaltilebox(0/1/2), which is not restricted at domain boundaries.SUBROUTINE coriolissetsi_start = MAX(ids+1,its)andi_end = MIN(ide-1,ite)underspecified/nested, with the j mirror, excluding both outermost faces. The same guard appears inperturbation_coriolisandcurvature.! boundary loops for coriolis not needed for open bdy (commented out 20100611 JD), overru_tend(its,k,j) = … *0.5*(f(its,j)+f(its,j))— the duplicating form, tried and discarded.latris exchanged with periodicity off (mp_exchange2d(..., .FALSE., .FALSE., ...)) whilefis exchanged withEWperiodic(ng), NSperiodic(ng)on — because wrapping a longitude jumps it 360° while wrapping a Coriolis parameter is correct.The invariant across WRF, WPS, ROMS, MPAS, ICON, MOM6 and NEMO is that no model lets an approximated latitude reach a Coriolis stencil unguarded: either it never differences
fthere, or it guards the one place it does. ERF currently does neither, so feeding it a corrected-but-extrapolated ghost would move it outside that invariant. Decoupling the arrays is the right long-term fix — ROMS and MOM6 both show what that looks like — but it is a larger change than this one and does not belong here.Consequence to be aware of:
sin_arr(i,ny) != sin(lat_m(i,ny))in that single ghost row, flagged in a comment at the site. Note also thatsolverChoice.variable_coriolisdefaults tofalse, so the ghost-differencing path requires an expliciterf.variable_coriolisin the deck.Verification, and its limits
wrfinput_d01: dimensions and the two coordinate values quoted above.convert → resize → & → copy → clamped growntileboxchain at the real grid dimensions, run before and after. Before:lon_m(117,50) = -85.38144, the duplicate. After:-85.0964. Max |after − file| over the entire valid region is 0, so valid-region values are bit-identical and the change is confined to the ghost row/column.-cobject compile of the changed translation unit at-O3with MPI and NetCDF, zero warnings; and again under a coupled driver's full define set.wrfinputdeck. A fullerf_srclibbuild was out of reach on the machine available. The fix is verified by compile plus a faithful runtime reproduction of the code path, not by a model run. Reviewers wanting an end-to-end check should exercise a real WRF-input case and confirmlon_m(nx,j) != lon_m(nx-1,j).Because the change is provably confined to the boundary ghosts, a bit-for-bit comparison of an existing WRF-input regression case should be unchanged; any interior difference would indicate the edge-locality argument is wrong and is the single most valuable thing to check.
Interaction with open pull requests
Checked against every open PR touching either file this change edits
(
ERF_InitFromWRFInput.cpp,ERF.H), for textual conflict and forsemantic interaction:
ERF_InitFromWRFInput.cpp:1688, entirely below this change. Its subject is base-state reconstruction (p_hse/pi_hse/th_hse/qv_hse), stopping the per-(i,j,k)copy of WRF'sPB/ALBin favour of evaluating WRF's analytic function at ERF'sz_phys_cc. It never reads the coordinate arrays. No interactionERF_InitFromWRFInput.cpp:449and:620straddle:486without touching it; net shift+5lines before:486,−2before:863. Semantically disjoint by construction: it makes no edit tosubdomain_to_fill_typed,var_fab.resize, theBox intersectioncopy, orixTypehandling, and all of itsinit_from_wrfinputlogic is name-gated toQCLOUD/QICE/QRAIN/QSNOW/QGRAUP, while this change is name-gated toXLONG_U/XLAT_V. Neither can fire on the other's variables. It replaces theNC_namesmoisture block at indices 24+, leavingXLAT_V // 22andXLONG_U // 23as unchanged context, so the coordinate variables keep their positional indicesERF_InitFromWRFInput.cpp,ERF.H:1595, convertingm_SurfaceLayerto an orientation-indexed container — below all hunks here, so it does not even shift them. ItsERF.Hhunks are at:1364and:1550, over a thousand lines from the:158doc comment here. No interaction with the coordinate arraysNo conflicts expected in any merge order, and the name-gating means the
separation survives future line movement rather than depending on current
offsets.
Worth noting for reviewers running a realistic case: #3884 adds
erf.use_wrf_bdy_qc_qianderf.bdy_moist_nudge_type = 3. A fully configuredreal WRF case is also the configuration most likely to exercise the corner mesh
this change repairs, since it is what puts genuine spatial structure into the
coupled surface fields.
Follow-ups, not addressed here
InitFromMetgridfillslon_m/lat_mfromXLONG_M/XLAT_M— genuine mass points, not staggered. The corner-mesh consumer's "already nodal in i" assumption is therefore simply wrong for metgrid-initialised runs, independently of this bug. Documented at the accessor rather than changed; probably deserves its own issue.i_hi/j_hiderive fromboxes_at_level[lev][0]whilevar_fabis sized fromboxes_at_level[lev][idx]. Identical for a single-box level 0; potentially wrong whennum_boxes_at_level > 1. Pre-existing and untouched.Provenance: the ERF, COAWST and driver citations above were read directly in-tree. The WRF, ROMS, MOM6, MPAS, ICON and NEMO comparisons come from a source survey of those repositories and are cited by file and symbol so a reviewer can check them; they were not independently re-derived here.