Skip to content

Add velocity-component reconstruction for I/O - #525

Draft
xylar wants to merge 7 commits into
E3SM-Project:developfrom
xylar:omega/add-velocity-components
Draft

Add velocity-component reconstruction for I/O#525
xylar wants to merge 7 commits into
E3SM-Project:developfrom
xylar:omega/add-velocity-components

Conversation

@xylar

@xylar xylar commented Aug 25, 2026

Copy link
Copy Markdown

Add ZonalVelocity and MeridionalVelocity as fields available for I/O in Omega. Omega carries only the edge-normal component of the velocity, so the zonal and meridional components at cell centers are reconstructed from it using the least-squares stencil and weights already supplied by the mesh file. The two new fields are registered with the auxiliary state and can be requested by any I/O stream. They are computed once per time step, and only when something asks for them. This also replaces the hardcoded placeholder value that surface coupling was exporting to the coupler as the surface velocity.


What this addresses

Omega is a C-grid model and stores only the edge-normal velocity, so there has never been a way to look at the velocity as a vector at cell centers. That has two consequences. Analysis and comparison against MPAS-Ocean output are awkward, since MPAS-Ocean writes velocityZonal and velocityMeridional. And more concretely, OcnToCplFields::updateFields had a // TODO: Implement vector reconstruction for velocity field and exported a hardcoded ConstSfcVelocity = 1e-4 to the coupler in place of a real surface velocity.

The machinery to fix this already landed in #480, which added the VectorReconOnCell operator plus the mesh-supplied stencil and weights (NEdgesReconOnCell, ReconStencilCell, ReconWeightsCell) that it needs. Nothing in src/ called it, and it only supported a single vertical layer on spherical meshes. This PR builds on that.

Direction of the solution

VectorReconOnCell gains an overload that takes a vertical index, so it can reconstruct one layer of a multi-layer field. It is the existing single-layer form with an index added, since the reconstruction is independent in each layer. The Cartesian to local geographic rotation is factored into a helper shared by both overloads. Note that neither the new overload nor anything else added here uses vertical chunking, in line with the direction of #473.

Whether the reconstruction is available is now treated as a property of the mesh file rather than of the geometry. The stencil and weights are precomputed as a mesh preprocessing step rather than by Omega, so Decomp detects their presence by attempting the read of NEdgesReconOnCell and treating a failure as "not present", and carries the answer as Decomp::HasVectorRecon and HorzMesh::HasVectorRecon. Previously all of this was gated on OnSphere, which meant planar meshes could never have it and forced Decomp to parse the OnSphere attribute itself, duplicating what HorzMesh does properly. That duplicate parsing is removed. The practical effect is that planar meshes work as soon as their mesh files carry the arrays, while planar meshes that predate them stay readable.

The fields themselves live in a new VelocityReconAuxVars auxiliary variable class, following the pattern of KineticAuxVars, and are registered into the existing AuxiliaryState field group. On a planar mesh the reconstructed vector already lies in the plane of the mesh, so the two variables hold the Cartesian x and y components instead of zonal and meridional ones.

They are driven by a new AuxiliaryState::computeVelocityRecon, deliberately kept out of computeAll. Nothing in the Omega equations reads these, so computing them once per time stepper stage would be wasted work; ocnRun calls them once per step instead, on the state the step just produced. They are also skipped entirely unless something needs them, which is decided by a new IOStream::isFieldRequested that reports whether a field is in the contents of any defined stream. Since validation is what expands a group name into its member fields, that answer is only meaningful after validation, so an unvalidated stream conservatively answers yes and the computation is done rather than skipped.

Surface coupling needs the reconstruction regardless of what the streams say, so SfcCoupling::init calls AuxiliaryState::requireVelocityRecon to force it on, and checks the mesh supplies the reconstruction data so a coupled run on a mesh without it fails at initialization rather than at the first time step. OcnToCplFields::updateFields then averages the real surface values in place of the 1e-4 placeholder.

Using the new fields

They are registered but are not in the contents of any stream by default, so add them to a stream to have them written:

      Contents:
        - VelocityZonalCell
        - VelocityMeridionalCell

Requesting them on a mesh whose file did not supply the reconstruction stencil and weights is an error, as is running coupled with such a mesh.

Tests

HORZ_OPERATORS_TEST gains a multi-layer check in testVectorRecon, alongside the existing single-layer one. Each layer holds the exact field scaled by a layer-dependent factor, so a reconstruction that mixed layers or reused a single layer shows up as an error. The error measures are normalized by the exact field, so a per-layer constant factor leaves them unchanged and the same expected error values apply as for the single-layer case.

SFC_COUPLING_TEST now checks the exported surface velocity instead of skipping it, comparing So_u and So_v against the reconstruction in the surface layer. At NAccumSteps == 0 the running average is exactly the single sampled value, so the comparison is exact, as it already is for temperature and salinity. This test now also initializes VertAdv and AuxiliaryState, which surface coupling depends on.

DECOMP_TEST gates its existing reconstruction stencil checks on HasVectorRecon rather than OnSphere.

Planar meshes

The code accepts planar meshes with reconstruction data, but no such mesh file exists yet, so the planar leg of testVectorRecon still returns early and this has only been exercised on spherical meshes. Generating one requires a Polaris fix: construct_rotation_matrix in polaris/mesh/reconstruct.py returned an all-ones matrix rather than the identity for planar meshes, which collapsed the local tangent frame and produced silently wrong weights. Reconstructing a uniform field on PlanarPeriodic48x48 gave a mean of (0.729, 0.729) for an exact (1.0, 0.5); with the identity it is exact to 2.7e-13. Weights on a spherical mesh are bit-identical before and after. That fix, with tests, is on the Polaris branch fix-planar-velocity-reconstruction. Enabling planar meshes here is left to a follow-up once it has merged and planar mesh files have been regenerated.

Checklist

  • Documentation:

  • Linting

  • Building

    • CMake build does not produce any new warnings from changes in this PR
  • Testing

    aurora, oneapi-ifx, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    chrysalis, oneapi-ifx, openmpi

    • CTests Pass
    • Polaris omega_pr Pass

    frontier, craygnu, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    frontier, craygnu-mphipcc, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    pm-cpu, gnu, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    pm-gpu, gnugpu, mpich

    • CTests Pass
    • Polaris omega_pr Pass
  • Provide relevant details in a comment to the PR titled Testing with the following:

    • Which machines CTest unit tests
      have been run on and indicate that are all passing.
    • The Polaris omega_pr test suite
      has passed, using the Polaris e3sm_submodules/Omega baseline
    • Document machine(s), compiler(s), and the build path(s) used for -p for both the baseline (Polaris e3sm_submodules/Omega) and the PR build
    • Indicate "All tests passed" or document failing tests
    • Document testing used to verify the changes including any tests that are added/modified/impacted.
  • Performance related PRs: Please include a relevant PACE experiment link documenting performance before and after.

  • New tests:

    • CTest unit tests for new features have been added per the approved design.
    • Polaris tests for new features have been added per the approved design (and included in a test suite)
  • Stealth Features

    • If any stealth features are included in the PR, please confirm that they have been documented.

xylar and others added 6 commits August 25, 2026 14:45
Add an overload that reconstructs one layer of a multi-layer field. The
reconstruction is independent in each layer, so this is the existing
single-layer form with a vertical index added.

Factor the Cartesian to local geographic rotation into a shared helper
so the spherical and planar paths stay identical between the two
overloads.

Extend the operator unit test to exercise the multi-layer form. Each
layer holds the exact field scaled by a layer-dependent factor so that
a reconstruction which mixed layers would show up as an error. The
error measures are normalized by the exact field, so the same expected
errors apply as for the single-layer case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reconstruction stencil and weights are precomputed as a mesh
preprocessing step rather than by Omega, so whether they are available
is a property of the mesh file, not of whether the mesh is spherical.
Detect their presence by attempting the read of NEdgesReconOnCell and
treating a failure as "not present", and carry the answer as
Decomp::HasVectorRecon and HorzMesh::HasVectorRecon.

This lets planar meshes carry reconstruction data while leaving planar
meshes that predate it readable, and makes VectorReconOnCell abort with
a message naming the three arrays it needs rather than refusing every
planar mesh. The planar branch already present in the operator becomes
reachable as a result.

Decomp no longer parses the OnSphere attribute, which existed only to
gate these reads; HorzMesh still determines it from the full mesh
stream as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add a VelocityReconAuxVars auxiliary variable class holding the zonal
and meridional velocity components reconstructed at cell centers from
the edge-normal velocity, and register both as fields in the
AuxiliaryState field group so they can be requested by any IO stream.
On a planar mesh the reconstructed vector already lies in the plane of
the mesh, so the two components are the Cartesian x and y components.

The reconstruction is written out here rather than calling
VectorReconOnCell so that layers outside MinLayerCell/MaxLayerCell keep
the fill value that Field::attachData wrote, which the operator, as a
reference implementation, knows nothing about.

Add AuxiliaryState::computeVelocityRecon to drive it. These are
diagnostics that no tendency reads, so the computation is deliberately
kept out of computeAll, which runs once per time stepper stage, and is
over owned cells only. It aborts if the mesh supplied no reconstruction
data. Nothing calls it yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add IOStream::isFieldRequested, which reports whether a Field is in the
contents of any defined stream, so a diagnostic that nothing will read
or write can be skipped. Since validate replaces a group name in a
stream's contents by its member Fields, this is only meaningful after
the streams are validated; an unvalidated stream conservatively answers
yes so an optional computation is done rather than skipped.

Use it to gate AuxiliaryState::computeVelocityRecon, resolving the
answer on the first call and caching it, since the streams are
validated only after all Fields are defined and so well after the
auxiliary state is constructed.

Call it once per time step from both ocnRun loops, on the state the
step just produced (time level 0), which is the state that
updateTimeLevels has attached to the NormalVelocity field and so the
one that any stream writing this step will write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the hardcoded 1e-4 placeholder in OcnToCplFields::updateFields
with the reconstructed zonal and meridional velocity in the surface
layer, and drop the TODO it carried.

Add AuxiliaryState::requireVelocityRecon so surface coupling can force
the reconstruction to be computed every step regardless of what the IO
streams ask for, and call it from SfcCoupling::init. That init also
now checks the mesh supplies the reconstruction data, so a coupled run
on a mesh without it fails at initialization rather than at the first
time step.

Initialize VertAdv and AuxiliaryState in SfcCouplingTest, which the
coupling now depends on, and check the exported surface velocity
against the reconstruction rather than skipping it. At NAccumSteps == 0
the running average is exactly the single sampled value, so the
comparison is exact, as it already is for temperature and salinity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add VelocityZonalCell and VelocityMeridionalCell to the auxiliary
variable list in the user guide, with a section on the three ways they
differ from the other auxiliary variables: they are diagnostic and so
computed once per step, they are only computed when a stream asks for
them or Omega is coupled, and they need a mesh file that supplies the
reconstruction stencil and weights.

Document computeVelocityRecon and requireVelocityRecon in the auxiliary
state developer guide, and IOStream::isFieldRequested in the IOStreams
developer guide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xylar

xylar commented Aug 25, 2026

Copy link
Copy Markdown
Author

This work is motivated by the need for reconstructed velocity components for analysis work in Polaris before the Sept. 15th deadline.

@xylar xylar changed the title Omega/add velocity components Add velocity-component reconstruction for I/O Aug 25, 2026
@xylar

xylar commented Aug 25, 2026

Copy link
Copy Markdown
Author

Needs to have the velocity reconstruction coefficients added to planar meshes for CTests, following the fix from E3SM-Project/polaris#721.

xylar added a commit to xylar/polaris that referenced this pull request Aug 25, 2026
PlanarPeriodic48x48.omega_vars.260518.nc has no vector-reconstruction
fields, so Omega's planar CTests cannot exercise the reconstruction.
The two spherical CTest meshes gained them in the 260807 update, but
the planar one was left behind because the weights it would have gotten
were wrong.

Point at PlanarPeriodic48x48.omega_vars.260825.nc, which is the same
mesh with NEdgesReconOnCell, ReconStencilCell and ReconWeightsCell
appended, computed after the two planar fixes earlier on this branch.
No pre-existing variable changed. Omega will pick up the same datestamp
in E3SM-Project/Omega#525.
PlanarPeriodic48x48.omega_vars.260825.nc is the first planar mesh to
carry the vector reconstruction stencil and weights, so the CTests that
use a planar mesh need it rather than the earlier datestamps.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xylar

xylar commented Aug 25, 2026

Copy link
Copy Markdown
Author

Early CTesting

CTest unit tests:

  • Machine: chrysalis
  • Compiler: intel
  • Build type: Release
  • Result: All tests passed
  • Log: /gpfs/fs1/home/ac.xylar/e3sm_work/polaris/fix-planar-velocity-reconstruction/build_omega/build_chrysalis_intel/ctests.log

@xylar
xylar requested a review from andrewdnolan August 25, 2026 21:41
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.

1 participant