Skip to content

Add submesoscale eddy parametrization - #438

Open
mwarusz wants to merge 12 commits into
E3SM-Project:developfrom
mwarusz:omega/submesoscale-eddies
Open

mwarusz wants to merge 12 commits into
E3SM-Project:developfrom
mwarusz:omega/submesoscale-eddies

Conversation

@mwarusz

@mwarusz mwarusz commented Jun 20, 2026 •

Copy link
Copy Markdown
Member

This PR adds the submesoscale eddy parametrization in the form presented in Fox-Kemper et al. 2011. The parametrization class provides three main methods that

  • determine mixed layer depth
  • compute buoyancy gradient
  • compute submesoscale induced velocity

respectively. A new auxiliary variable named NormalTransportVelocity has been added. This velocity variable is used to advect pseudo-thickness and tracers, and can optionally include the submesocale induced velocity.

Checklist

  • Documentation:
  • Linting
  • Building
    • CMake build does not produce any new warnings from changes in this PR
  • Testing
    • Add 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.
    • New tests:
      • CTest unit tests for new features have been added per the approved design.

@mwarusz

mwarusz commented Jun 20, 2026 •

Copy link
Copy Markdown
Member Author

The parametrization unit tests are passing. However, after turning it on in the baroclinic channel polaris test with the default parameters, I see NaNs after about 3 hours of simulation time, so there is still some debugging to be done. Moreover, user and developer documentation needs to be added.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new submesoscale eddy parameterization module (Fox-Kemper
et al. 2011) into Omega’s ocean model, wires it into initialization/finalization
and tendency/auxiliary-state workflows, and adds a dedicated unit test plus
broad test-harness updates to support new analytic-field helper signatures.

Changes:

  • Add SubmesoEddies module (config-driven) and integrate it into ocean module
    init/finalize and auxiliary-state computation.
  • Introduce TransportAuxVars::NormalTransportVelocity and use it in thickness
    and tracer tendency computations as the transport velocity (optionally
    including submesoscale-induced velocity).
  • Update unit test utilities and many tests to accommodate updated analytic
    field setters; add SubmesoEddiesTest.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
components/omega/test/timeStepping/TimeStepperTest.cpp Initialize/destroy SubmesoEddies in the time stepper test harness.
components/omega/test/ocn/TendencyTermsTest.cpp Update analytic-field lambdas to match updated setScalar/setVectorEdge signatures.
components/omega/test/ocn/TendenciesTest.cpp Initialize/destroy SubmesoEddies; update analytic-field lambdas and setScalar/setVectorEdge call signatures.
components/omega/test/ocn/SubmesoEddiesTest.cpp New unit test covering mixed-layer depth, buoyancy gradient, and eddy velocity computations.
components/omega/test/ocn/OceanTestCommon.h Extend setScalar/setVectorEdge helpers to pass element indices and optional Z coordinates.
components/omega/test/ocn/HorzOperatorsTest.cpp Update analytic-field lambdas to match updated helper signatures.
components/omega/test/ocn/AuxiliaryVarsTest.cpp Update analytic-field lambdas to match updated helper signatures.
components/omega/test/ocn/AuxiliaryStateTest.cpp Initialize/destroy SubmesoEddies; update analytic-field lambdas and helper call signatures.
components/omega/test/CMakeLists.txt Register new SUBMESOEDDIES_TEST.
components/omega/src/timeStepping/ForwardBackwardStepper.cpp Pass tracer array into thickness tendency computation.
components/omega/src/ocn/Tendencies.h Extend thickness tendency API to accept a tracer array (for aux-state computations).
components/omega/src/ocn/Tendencies.cpp Route thickness/tracer tendencies through new aux-state compute routines and use NormalTransportVelocity.
components/omega/src/ocn/SubmesoEddies.h New SubmesoEddies public interface.
components/omega/src/ocn/SubmesoEddies.cpp New implementation and IO field registration for SubmesoEddies.
components/omega/src/ocn/OceanInit.cpp Initialize SubmesoEddies during ocean module init.
components/omega/src/ocn/OceanFinal.cpp Destroy SubmesoEddies during ocean finalize.
components/omega/src/ocn/auxiliaryVars/TransportAuxVars.h New aux-var container for NormalTransportVelocity.
components/omega/src/ocn/auxiliaryVars/TransportAuxVars.cpp Register/unregister IO field for NormalTransportVelocity.
components/omega/src/ocn/AuxiliaryState.h Add TransportAuxVars member and new aux compute entry points.
components/omega/src/ocn/AuxiliaryState.cpp Compute Brunt–Väisälä frequency, compute transport velocity (optionally + submeso), and use it for vertical pseudo-velocity.
components/omega/configs/Default.yml Add Omega.Submeso configuration group and defaults.

Comment thread components/omega/src/ocn/AuxiliaryState.cpp Outdated
Comment on lines +173 to +182
KOKKOS_LAMBDA(int IEdge, const TeamMember &Team) {
const int KMin = MinLayerEdgeBot(IEdge);
const int KMax = MaxLayerEdgeTop(IEdge);
const int KRange = vertRangeChunked(KMin, KMax);

parallelForInner(
Team, KRange, INNER_LAMBDA(int KChunk) {
LocPseudoThicknessAux.computeVarsOnEdge(
IEdge, KChunk, PseudoThick, NormalVelEdge);
});
Comment on lines +18 to +22
/// Destroy instance of SubmesoEddies
void SubmesoEddies::destroyInstance() {
delete Instance;
Instance = nullptr;
}
Comment thread components/omega/src/ocn/SubmesoEddies.cpp
Comment thread components/omega/src/ocn/SubmesoEddies.cpp
Comment thread components/omega/test/ocn/SubmesoEddiesTest.cpp
@mwarusz
mwarusz force-pushed the omega/submesoscale-eddies branch 2 times, most recently from 1d3f283 to 9825eda Compare July 14, 2026 21:14
@mwarusz

mwarusz commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Testing

CTest unit tests

  • Machine: pm-cpu / pm-gpu / frontier
  • Compiler: gnu / gnugpu / craygnu-mphipcc
  • Build type: Release
  • Result: All tests passed (on all machines)

Polaris omega_pr suite

  • Baseline workdir: /pscratch/sd/m/mwarusz/omega-pr-testing/submeso/baseline-gnu
  • Baseline build: /pscratch/sd/m/mwarusz/omega-pr-testing/submeso/baseline-gnu/build
  • PR build: /pscratch/sd/m/mwarusz/omega-pr-testing/submeso/build-gnu
  • PR workdir: /pscratch/sd/m/mwarusz/omega-pr-testing/submeso/pr-gnu
  • Machine: pm-cpu
  • Compiler: gnu
  • Build type: Release
  • Log: /pscratch/sd/m/mwarusz/omega-pr-testing/submeso/pr-gnu/polaris_omega_pr.o55955301
  • Result: All tests passed

@mwarusz

mwarusz commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

The baroclinic channel test is now running stably with the submesoscale eddy parametrization turned on. This PR is ready for review. However, I haven't checked that the parametrization has the intended effect, and I'm not sure that I'm the best person to do that. @vanroekel could you help with this kind of testing ?

@mwarusz
mwarusz requested review from katsmith133 and vanroekel July 15, 2026 23:29
@vanroekel

Copy link
Copy Markdown
Collaborator

@mwarusz I ran a couple tests with baroclinic channel and it's too hard to tell what the effects must be. We need to add a mixed layer. Let me look and see how straight forward that might be.

@vanroekel

Copy link
Copy Markdown
Collaborator

update - I have a mixed layer variant of baroclinic channel I have a test run in the queue on frontier. Hopefully will have results by Monday

PseudoThickML, GradBuoyML, BVFreqML);

GradBuoyML /= PseudoThickML;
BVFreqML /= PseudoThickML;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwarusz for small ML this could be be problematic, it's not clear above in the MLD code if there is a floor. maybe it's not an issue though

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that we should probably add a tiny value here. MPAS-O has it.

Team, Range{MinLyrEdgeBot, IndexMLEdge},
INNER_LAMBDA(int K, Real &AccumThick, Real &AccumGradBuoy,
Real &AccumBVFreq) {
const Real PseudoThickKm1 =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If your function starts at MinLyrEdgeBot can this lead to values lower than min layer edge?

@mwarusz mwarusz Aug 11, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this variable is set to

((K - 1) >= MinLyrEdgeBot ? MeanPseudoThickEdge(IEdge, K - 1) : 0)

which is 0 for K = MinLyrEdgeBot. In the end an average of PseudoThickKm1 and PseudoThickK is used. This means that for K = MinLyrEdgeBot you get

PseudoThickAvg = 0.5 * (0 + MeanPseudoThickEdge(IEdge, MinLyrEdgeBot));

This is a kinda hacky treatment of the first half layer. I will add a comment to explain this.

@vanroekel

Copy link
Copy Markdown
Collaborator

@mwarusz in testing I'm getting NaN for EddyVelocity quickly. CoPilot added some guards on the ML average and zero initializing and it fixed the NaN issue. I can point you at what the changes made were if you'd like

@mwarusz

mwarusz commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@vanroekel

@mwarusz in testing I'm getting NaN for EddyVelocity quickly. CoPilot added some guards on the ML average and zero initializing and it fixed the NaN issue. I can point you at what the changes made were if you'd like

Please do.

@vanroekel

Copy link
Copy Markdown
Collaborator

here it is - https://github.com/vanroekel/E3SM/tree/temp-submeso-branch

There is some extra stuff that is adding more diagnostics as well.

@mwarusz

mwarusz commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Hmm, Copilot added this check

if (PseudoThickML > Tiny && MLDepthEdge > Tiny)

but afterwards it is also explicitly checking for NaNs and skipping calculations if it finds any. It would good to see if the first check is enough.

@mwarusz
mwarusz force-pushed the omega/submesoscale-eddies branch 2 times, most recently from 3660296 to a7bd36d Compare August 24, 2026 22:33

// Compute mixed layer index and depth based on the density difference
// criterion
void computeDenMixLayerDepth(const Array2DReal &SpecVol);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwarusz, you were going to move this to an aux variable, correct?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I haven't worked on this PR in a while. I was planning to get back to it after the split-explicit is merged, since the transport velocity refactoring that I did there affects this PR in non-trivial ways.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Thanks!

@mwarusz
mwarusz force-pushed the omega/submesoscale-eddies branch from a7bd36d to 0bf23ad Compare September 18, 2026 23:10
@mwarusz

mwarusz commented Sep 21, 2026

Copy link
Copy Markdown
Member Author

To test this branch after rebasing on develop and fixing a couple of bugs I ran ocean/spherical/realistic_global/EC30to60E2r2/analysis_members_test for about one year using Omega and MPAS-O with the following changes to both models:

  • enable submesoscale eddy parametrization
  • use split-explicit time stepping (RK2 with 45 min time step for Omega, AB2 with 30 min time step for MPAS-O)
  • use monotonic third-order horizontal advection

Here are some plots comparing temperature, mixed layer depth, and eddy velocity kinetic energy at the final time. @vanroekel Do you think these look reasonable ?

Omega

temperature_360days DenMixLayerDepth_360days EddyKineticEnergy_360days

MPAS-O

temperature_360days dThreshMLD_360days EddyKineticEnergy_360days

Comment thread components/omega/doc/userGuide/SubmesoEddies.md Outdated
Comment thread components/omega/doc/userGuide/SubmesoEddies.md Outdated
Comment thread components/omega/doc/userGuide/SubmesoEddies.md Outdated
Comment thread components/omega/src/ocn/auxiliaryVars/MixedLayerAuxVars.h Outdated
- `Tau`: MLI timescale parameter (s).
- `Ce`: nondimensional efficiency coefficient.
- `LfMin`: minimum frontal width limiter (m).
- `DsMax`: maximum grid-length limiter used in the closure (m).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somewhere, either here or in the SubmesoEddy class we should include reasonable ranges of these parameters.

int NDims = 2;
std::vector<std::string> DimNames(NDims);
DimNames[0] = "NEdges";
DimNames[1] = "NVertLayers";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is interface - should this be NVertLayersP1?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines are for EddyVelocity, which is not located at interfaces.


parallelForOuter(
LaunchConfig({Mesh->NEdgesAll},
TeamScratch<Real>(3 * NVertLayers + 3 * NVertLayersP1)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain the 3 * here? I don't understand where they come from

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the number of scratch arrays used in this loop. Three scratch arrays for layer data and three for interface data. I added a comment and changed the code slightly to hopefully make it more clear.

const int K = MaxLyrEdgeTop + 1;
const int JCell0 = CellsOnEdge(IEdge, 0);
const int JCell1 = CellsOnEdge(IEdge, 1);
BVFSqEdge(K) = 0.5_Real * (BruntVaisalaFreqSq(JCell1, K) +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this part different than just above on L183?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not. In addition to BVF, the loop above interpolates fields that are located at layer mid-points (SpecVol), so it only loops over layers. This code handles the last interface interpolation for BVF. I rephrased the comment above to make it more clear.

@vanroekel

Copy link
Copy Markdown
Collaborator

@mwarusz this looks pretty good to me. Most of my comments are clarification questions. Your results look pretty good between the two. I hope the MLD difference is without submeso - with. Submeso should shallow MLD everywhere.

@katsmith133

Copy link
Copy Markdown

@mwarusz this looks pretty good to me. Most of my comments are clarification questions. Your results look pretty good between the two. I hope the MLD difference is without submeso - with. Submeso should shallow MLD everywhere.

Where is this MLD difference plot? I don't see it. I see the MPAS and Omega MLD plots, but they seem like they are just straight up MLD, not differences... unless I am mistaken. I think a difference plot would be useful to see. Maybe just a difference between an average of the last 3 months of the run.

@vanroekel

Copy link
Copy Markdown
Collaborator

Oh yes, sorry @katsmith133 I thought the middle plot was a diff, but the positive didn't make sense. Now it does since it is MLD.

I agree, and would add a difference plot of MLE on and off would be super helpful in each model in addition to a MPAS-Omega diff.

@mwarusz

mwarusz commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

Thanks for your suggestions @katsmith133 and @vanroekel . I will create such plots.

Co-authored-by: Luke Van Roekel <lvanroekel@lanl.gov>

@katsmith133 katsmith133 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this @mwarusz! I just have a few questions and I think the docs still need to be updated now that the MLD calculation has been moved.

Comment thread components/omega/doc/devGuide/SubmesoEddies.md Outdated
EosInstance->computeDepthMeanSpecificVolume(PseudoThickCell);

// compute Brunt-Vaisala freqency squared
EosInstance->computeBruntVaisalaFreqSq(ConservTemp, AbsSalinity, PressureMid,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is a part of Omega I don't completely understand. I see here that BVF is being computed again, but with pressure at the mid layer, does that then overwrite the previous BVF that was calculated at the interface in a different part of the code? And which one gets outputted?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at every time step there is only this calculation of BVF and one in implicit vertical mixing, that happens after the state update. Which one gets outputted depends on the config options, and is a known issue that we should fix (#532). As for the use of PressureMid, I assumed this is what the BVF code expects, based on the following lines

Real PInt =
0.5_Real * (Pressure(ICell, K) + Pressure(ICell, K - 1));

I see now that the call in implicit vertical mixing uses PressureInterface
EqState->computeBruntVaisalaFreqSq(
ConservTemp, AbsSalinity, VCoord->PressureInterface, EqState->SpecVol);

Is this a bug ?


// Generic linear interpolation routine. This should be in OmegaMath.h or
// something like that once it exists.
KOKKOS_INLINE_FUNCTION Real linearInterp(Real x, Real y1, Real x1, Real y2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alicebarthel's frazil PR (#466) also includes a function like this that should be put into something like OmegaMath.h. Should we make a separate PR for this or just include the new file in this PR or #466?

}
}

void SubmesoEddies::computeTimeScale() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be done as a separate NEdgesAll loop? Seems like this could be calculated when its needed and save the extra compute time? Maybe I am missing something

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is calculating it once in its own loop less computation than having to calculate it every time in another loop with other things? I'm guessing that is the case since this is only called once in the init.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly.

Team, NVertLayersP1,
INNER_LAMBDA(int K) { StreamFunction(K) = 0; });

const Real MLDepthEdge = Kokkos::min(DenMixLayerDepth(JCell0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In MPAS we are taking the average of these two cells, but here its the minimum... what is the reason for the change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MPAS-O takes the minimum for the mixed-layer index here

indMLDedge = min(indMLD(cell1),indMLD(cell2))

but then, like you said, uses the average depth
zMLD = 0.5_RKIND*(dThreshMLD(cell1)+dThreshMLD(cell2))

Me and @vanroekel looked at the MPAS-O code a while back and we thought it was better to be consistent here.

This branch has not been deployed

No deployments
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.

6 participants