Skip to content

[WIP] add KPP mixing - #512

Draft
vanroekel wants to merge 36 commits into
E3SM-Project:developfrom
vanroekel:vanroekel/omega/kpp-final
Draft

[WIP] add KPP mixing#512
vanroekel wants to merge 36 commits into
E3SM-Project:developfrom
vanroekel:vanroekel/omega/kpp-final

Conversation

@vanroekel

@vanroekel vanroekel commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

This PR adds the mixing scheme of Large et al (1994), the K-Profile Parameterization (KPP) to omega. It includes the local and non-local components and theory wave langmuir mixing. The code

  1. computes the boundary layer depth in a manner similar to MPAS-Ocean and CVMix including boundary layer base search with quadratic interpolation
  2. computes a surface layer average consistent with MPAS-Ocean
  3. implements non-local forcing split from the implicit solve as is common
  4. It merges mixing coefficients from each parameterization (background, convection, shear instability driven mixing) similarly to mpas-ocean

The bulk of the code is written through co-pilot with physics guidance and corrections by me.

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)

alicebarthel and others added 25 commits August 11, 2026 14:04
Add KPP vertical mixing support on top of thermo forcing capability
@vanroekel

Copy link
Copy Markdown
Collaborator Author

This PR is rebased on #461 so it has extra changes. Anything related to forcing can be ignored here

@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 job on this @vanroekel! This must have been a lot of work.

This was just my first pass as this was a lot of code to go through, but its a place to start. I think in addition to the specific comments I made below, some commenting throughout the KPP guts would be helpful for me to review more in depth. There are a lot of variable names that are ambiguous, so I am not sure what is being calculated. I haven't done the job of going back and forth between the CVMix code and this code to make sure its all of the guts are exactly correct. I think the single column testing might actually do a better job of catching those or letting us know there is a problem with them in the first place. More so I looked at how everything (functions, variables, etc) was being used and passed around from a higher level.

Also, a lot of the variable names don't conform to the Omega standard. I'm guessing because they were direct variable name ports from CVMix?

Lastly... I think @mwarusz input on how things are called from a computational perspective will be super helpful here as it was a lot for me to go through and I am sure I did not catch those things.

Comment thread components/omega/doc/design/KPPMix.md Outdated
Comment thread components/omega/doc/userGuide/KPPMix.md
Comment thread components/omega/doc/devGuide/KPPMix.md
Comment thread components/omega/doc/userGuide/KPPMix.md Outdated
Comment thread components/omega/doc/devGuide/KPPMix.md
Comment thread components/omega/src/ocn/Tendencies.cpp
Comment thread components/omega/src/ocn/Tendencies.cpp Outdated
Array2DReal NormalVelEdge = State->getNormalVelocity(VelTimeLevel);

Array1DReal SurfacePressure("KPP-SurfacePressure", NCellsAll);
deepCopy(SurfacePressure, 1.0e5_Real);

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 override the Fill Values?

Comment thread components/omega/src/ocn/Tendencies.cpp Outdated
const Real ct_freezing =
Eos::calcCtFreezing(LocEosChoice, surface_salinity,
PressureMid(ICell, KSurf) * Pa2Db, 0.0_Real);
const Real heat_flux =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We will probably want to update these once the final equation is decided upon in #461.

Also, all of this is already computed in TendencyTerms.h by #461... can we use the temp flux and salt flux from there instead to save computation and also to lessen the risk of these calculations not matching if say the thermo forcing assumptions are updated in the future, then they don't have to be updated in multiple places? Maybe we can save surface temp flux and salt flux as variables calculated in TendencyTerms.h that can be passed in to here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, @alicebarthel and I discussed the same things. I have a potential change to share code now that I'm posting soon here

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What was the reason for moving this to the .cpp side? And was the equivalent code in VertMix.h removed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was a mistake in a copilot PR, I'm working to fix this now

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Actually what is 'this'? I see some computation here but I think that was added in the tendency hookup not in this PR. I just added to it.

for (int KVec = 0; KVec < KLen; ++KVec) {
const I4 K = KStart + KVec;
const bool ApplyConv =
(!LocKPPEnabled) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe I'm not understanding something, but why is this logic applied to just convective adjustment and not the shear mixing part too?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is an assumption made in KPP that convective diffusivity should not be counted in the BL since it roughly resembles the non local flux, so there would be double counting. I should add this to the docs.

Comment thread components/omega/src/ocn/KPPMix.cpp Outdated
StopOBLSearch only rescaled the bulk Richardson crossing threshold, it never
terminated the OBL search. At its default of 1.0 the threshold equals
CriticalBulkRichardsonNumber exactly, so the option duplicated an existing
knob.

Decoupling the two is also physically inconsistent: RiCritical normalizes the
unresolved shear Vt^2 in Large et al. (1994) Eq. 23, which is derived assuming
the same critical Richardson number used for the crossing test. Use
CriticalBulkRichardsonNumber to adjust the OBL depth criterion instead.

This is answer preserving since every configuration used the default value.
The bulk Richardson search reset the surface-layer reference averages to the
top of the column at every trial depth and then replayed the accumulation
downward, making the search O(K^2) per column, and O(K^2*nEdges) for the
per-edge velocity averages.

The averaging window spans the top epsilon*d, which grows monotonically with
the trial depth, so the window end only ever moves downward. Hoist the running
sums and their level pointers out of the trial-depth loop so the accumulation
becomes a two-pointer scan that is amortized O(1) per level.

This is bit for bit identical since the accumulation order from the surface is
unchanged, it is simply no longer repeated.
The bulk Richardson search traversed every model layer even after the crossing
was found. Only the crossing level and the two above it feed the quadratic
refinement of the boundary layer depth, so the remaining levels contributed
nothing to the diagnosed depth or to any mixing coefficient.

Break out of the loop once the crossing is latched. The four Ri diagnostic
fields written inside the loop, BulkRichardsonNumber, BulkRichardsonShear,
UnresolvedShear and BuoyancyJump, are consequently zero below the boundary
layer base; they are zero filled before the kernel, so the values remain well
defined. Setting DebugDiagnostics suppresses the early exit and restores the
full water column profiles for those fields.

BoundaryLayerDepth and all mixing coefficients are unchanged.
@mark-petersen

mark-petersen commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

This KPP branch works great on the SOMA test case!

SOMA after 4 months, 32km resolution. RK4 with 2 min timestep, viscosity del4 of 2e11. No explicit tracer diffusion.
Omega No KPP
image
MPAS-Ocean No KPP
image

The instability creeps northward from the southern boundary where cooler water is being advected upwards.

KPP saves the day! Using this branch with linear EOS on both (but JM/TEOS10 looks the same)
Omega with KPP
image
MPAS-O with KPP
image

I used default KPP flags in both models. Thank you @vanroekel for this addition! Thanks to @hyungyukang for his consultation to get this working.

@mark-petersen

Copy link
Copy Markdown
Collaborator

You should remove these. They are printing with every RHS compute

KPPMix.cpp:922:   LOG_INFO("KPPMix::computeOBLDepth: OBL depth computed");
KPPMix.cpp:1245:   LOG_INFO("KPPMix::computeMixingCoefficients: Phase 2 mixing coefficients "

Factor repeated KPP device math into KPPConstants.h helpers, decouple the
non-local flux shape from the coefficient matching option, and correct
several array extents that made KPP abort on multi-rank runs.

Shared device math (answer-preserving):
- Add kppTurbScales, kppMatchShape, kppNonLocalCs, kppClampOBLDepth and
  kppOBLIndex to KPPConstants.h.
- Replace the six duplicated blocks in KPPMix.cpp: two turbulent-scale
  computations, two match-shape computations, the inline C_s constant, and
  two OBL clamp plus index-search loops. Collapse the repeated
  UseInteriorMix && MatchBoth predicate into LocUseMatchedShapes.

Non-local shape (changes answers under MatchBoth only):
- Always build the non-local flux from the unmatched scalar shape rather
  than reusing the diffusivity shape. The matched shape is non-zero at the
  boundary layer base by construction, so gamma was finite there and then
  dropped discontinuously to zero immediately below. CVMix and MPAS-Ocean
  likewise treat matching as a diffusivity choice, separate from the
  non-local shape.
- Update testMatchBothInteriorCoefficients, which previously asserted both
  the matched value inside the boundary layer and zero at its base. Those
  two expectations cannot both hold.
- Update design documentation, which had described the shared shape as
  intentional.

Array extents (answer-preserving):
- Size the KPP scratch arrays in Tendencies::computeKPPFields and the
  boundary layer index copy in VertMix::computeVertMix with NCellsSize
  rather than NCellsAll, matching the KPPMix members they are copied to
  and from. Kokkos rejects mismatched deep_copy extents, so KPP aborted on
  any run where NCellsAll and NCellsSize differ. Single-rank tests could
  not detect this.
- Size the KPPMixTest scratch and expected buffers the same way.

Add unit tests for the new helpers. Full Omega suite passes 55/55 on
pm-cpu with gnu.
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.

5 participants