Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions components/omega/configs/Default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ Omega:
PressureGradTendencyEnable: true
VelVertMixTendencyEnable: true
TracerVertMixTendencyEnable: true
HorzMesh:
MeshScaling:
ScaleWithMesh: false

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 personally think we should get rid of ScaleWithMesh entirely and not have any support for MeshDensity. The tools for mesh creation now just fill it with all ones. so it is not going to be useful in new meshes. The term ScaleWithMesh is super confusing because it seems like it means UseRefWidth.

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 thought this is what we already agreed to in #476 (comment)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@xylar , thanks. Right. I kept that part of the code just in case, mainly for comparison with MPAS-Ocean. Since UseRefWidth: true is the default, that approach will not be used. However, I agree that we should remove it to avoid any confusions and potential issues. Will work on it.

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 think we can keep it on a testing branch or something for comparison to MPAS-Ocean but I don't want it to become part of the Omega code unless it's buried enough that no user will try to use it.

UseRefWidth: true
RefWidth: 30.0e3

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'm not sure that this is a good idea because it would be easy for users (or buildnml) to neglect to change this option and thus end up applying a different diffusivity than intended. Could this maybe be computed online (at init) as the median cell width? It's not clear to me that this needs to be user-configurable since the user can just change the diffusivities.

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 also agree. The MPAS-Ocean equivalent settings have gotten me in trouble so many times. I like the median resolution idea. I think MPAS-Ocean has a setting where it uses the minimum but that's a noisy value. The median would be a lot more robust -- just requiring a global reduction at init.

@hyungyukang hyungyukang Aug 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@cbegeman and @xylar , thanks for your thoughts on this. I agree. I also had some bad experiences with these settings.

To summarize next steps,

  • Removal of MeshScaling setting in omega.yml
  • Removal of MeshDensity related MeshScaling computations
  • Compute the global median cell width ($\Delta x_{med}$) at initialization
  • Compute MeshScalingDel2 and MeshScalingDel4 from $\Delta x_{med}$:

$$ MeshScalingDel2 = \frac{\Delta x}{\Delta x_{med}} $$ $$ MeshScalingDel4 = \left(\frac{\Delta x}{\Delta x_{med}} \right)^3 $$

Do you agree with this plan?

As far as I understand, in MPAS-Ocean, the Del2 and Del4 viscosity coefficients for each horizontal resolution are tuned based on the minimum grid spacing. One concern I have is that $\Delta x_{med}$ on a variable-resolution mesh may be larger than the minimum grid spacing, or its theoretical value. Also, $\Delta x_{med}$ may differ between meshes even if they share the same theoretical minimum grid spacing, for example, IcoswISC30E3r5 and EC30to60E2r2. This suggests that we may need to retune the Del2 and Del4 viscosity coefficients for Omega for each different mesh? Please let me know if I am misunderstanding anything.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One concern I have is that $\Delta x_{med}$ on a variable-resolution mesh may be larger than the minimum grid spacing, or its theoretical value.

@hyungyukang, yes, that would be true by definition, so the coefficient would need to be adjusted accordingly. But the median resolution should be much more robust across different meshes that are generated at ostensibly the same resolution (because of noise in the Jigsaw generation process).

The expectation should not be that the same coefficient is right for IcoswISC30E3r5 and EC30to60E2r2, though they wouldn't differ by too much. I helpful piece of information for debugging would be to have Omega output:

  • $\Delta x_{med}$
  • the min and max values of the del2 and del4 coefficients.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do you agree with this plan?

Yes, I agree.

MaxMeshDensity: -1.0
ManufacturedSolution:
WavelengthX: 5.0e6
WavelengthY: 4.33013e6
Expand Down
24 changes: 24 additions & 0 deletions components/omega/doc/userGuide/HorzMesh.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ variables. These include ``OnSphere`` (or ``on_a_sphere`` for backcompatibility)
OnSphere and IsPeriodic are stored as YES/NO strings in the metadata but as
boolean flags in the code.

### Horizontal mixing mesh scaling

The `HorzMesh: MeshScaling` configuration controls whether Laplacian and
biharmonic mixing coefficients vary with horizontal mesh size:

```yaml
HorzMesh:
MeshScaling:
ScaleWithMesh: false
UseRefWidth: false
RefWidth: 30.0e3
MaxMeshDensity: -1.0
```

When `ScaleWithMesh` is false, both scaling coefficients are one. When it is
true and `UseRefWidth` is true, Omega computes the effective cell width at
each edge from the areas of its two adjacent cells. The Laplacian scaling is
the ratio of this width to `RefWidth`, and the biharmonic scaling is the
cube of that ratio.

When `UseRefWidth` is false, Omega uses the legacy MPAS-Ocean scaling based
on `MeshDensity`. A negative `MaxMeshDensity` requests that Omega compute its
global value from the mesh during initialization.

In the future, the Mesh class will optionally compute the mesh variables that
are dependent on the Cartesian mesh coordinates internally.
This includes the various areas, lengths, angles, and weights needed for the
Expand Down
107 changes: 99 additions & 8 deletions components/omega/src/ocn/HorzMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//===----------------------------------------------------------------------===//

#include "HorzMesh.h"
#include "Config.h"
#include "DataTypes.h"
#include "Decomp.h"
#include "Dimension.h"
Expand All @@ -19,6 +20,7 @@
#include "Halo.h"
#include "IOStream.h"
#include "OmegaKokkos.h"
#include "Reductions.h"

namespace OMEGA {

Expand Down Expand Up @@ -390,19 +392,108 @@ void HorzMesh::computeEdgeSign() {

//------------------------------------------------------------------------------
// Set mesh scaling coefficients for mixing terms in momentum and tracer
// equations so viscosity and diffusion scale with mesh.
// equations so viscosity and diffusion scale with mesh, following MPAS-Ocean
void HorzMesh::computeMeshScaling() {

Config *OmegaConfig = Config::getOmegaConfig();
Config HorzMeshConfig("HorzMesh");
Error Err = OmegaConfig->get(HorzMeshConfig);
CHECK_ERROR_ABORT(Err,
"HorzMesh: HorzMesh group not found in configuration");

Config MeshScalingConfig("MeshScaling");
Err = HorzMeshConfig.get(MeshScalingConfig);
CHECK_ERROR_ABORT(Err,
"HorzMesh: MeshScaling group not found in configuration");

bool ScaleWithMesh;
bool UseRefWidth;
Real MaxMeshDensity;
Real RefWidth;

Err += MeshScalingConfig.get("ScaleWithMesh", ScaleWithMesh);
Err += MeshScalingConfig.get("UseRefWidth", UseRefWidth);
Err += MeshScalingConfig.get("RefWidth", RefWidth);
Err += MeshScalingConfig.get("MaxMeshDensity", MaxMeshDensity);
CHECK_ERROR_ABORT(Err, "HorzMesh: error reading mesh scaling configuration");

OMEGA_SCOPE(o_MeshScalingDel2, MeshScalingDel2);
OMEGA_SCOPE(o_MeshScalingDel4, MeshScalingDel4);

// TODO: implement mesh scaling by cell area, only no scaling
// option for now
parallelFor(
{NEdgesAll}, KOKKOS_LAMBDA(int Edge) {
o_MeshScalingDel2(Edge) = 1.0;
o_MeshScalingDel4(Edge) = 1.0;
});
if (ScaleWithMesh) {
if (UseRefWidth) {
OMEGA_REQUIRE(RefWidth > 0.0_Real,
"HorzMesh: RefWidth must be positive, got {}", RefWidth);

OMEGA_SCOPE(o_RefWidth, RefWidth);
OMEGA_SCOPE(o_AreaCell, AreaCell);
OMEGA_SCOPE(o_CellsOnEdge, CellsOnEdge);

// Mesh scaling is computed using CellWidth derived from AreaCell
// and the input reference width, RefWidth (see Eqs. (1) and (2) of
// Hoch et al. 2020, JAMES).
parallelFor(
{NEdgesAll}, KOKKOS_LAMBDA(int Edge) {
const int Cell0 = o_CellsOnEdge(Edge, 0);
const int Cell1 = o_CellsOnEdge(Edge, 1);
const Real CellWidth =
2.0_Real *
Kokkos::sqrt((o_AreaCell(Cell0) + o_AreaCell(Cell1)) /
(2.0_Real * Pi));
const Real Del2Scale = CellWidth / o_RefWidth;

o_MeshScalingDel2(Edge) = Del2Scale;
o_MeshScalingDel4(Edge) = Del2Scale * Del2Scale * Del2Scale;
});

} else {
// Mesh scaling is set by MeshDensity. This is both confusing
// and inconvenient, as the flags like ViscDel2 need
// to be reset for every resolution. It is kept for backwards
// compatibility, but should become defunct.
// Del2 scales as Dc**1, Del4 scales as Dc**3
if (MaxMeshDensity < 0.0_Real) {
Real MaxMeshDensityLocal = 0.0_Real;
for (int Cell = 0; Cell < NCellsOwned; ++Cell)
MaxMeshDensityLocal =
std::max(MaxMeshDensityLocal, MeshDensityH(Cell));

Halo *HorzMeshHalo = Halo::get(MeshName);
MaxMeshDensity =
globalMaxVal(MaxMeshDensityLocal, HorzMeshHalo->getComm());
MeshScalingConfig.set("MaxMeshDensity", MaxMeshDensity);
}

OMEGA_REQUIRE(MaxMeshDensity > 0.0_Real,
"HorzMesh: MaxMeshDensity must be positive, got {}",
MaxMeshDensity);

OMEGA_SCOPE(o_CellsOnEdge, CellsOnEdge);
OMEGA_SCOPE(o_MeshDensity, MeshDensity);
OMEGA_SCOPE(o_MaxMeshDensity, MaxMeshDensity);

parallelFor(
{NEdgesAll}, KOKKOS_LAMBDA(int Edge) {
const int Cell0 = o_CellsOnEdge(Edge, 0);
const int Cell1 = o_CellsOnEdge(Edge, 1);
const Real AvgDensity =
0.5_Real * (o_MeshDensity(Cell0) + o_MeshDensity(Cell1));
const Real DensityRatio = AvgDensity / o_MaxMeshDensity;

o_MeshScalingDel2(Edge) =
1.0_Real / Kokkos::pow(DensityRatio, 0.25_Real);
o_MeshScalingDel4(Edge) =
1.0_Real / Kokkos::pow(DensityRatio, 0.75_Real);
});
} // if UseRefWidth

} else {
parallelFor(
{NEdgesAll}, KOKKOS_LAMBDA(int Edge) {
o_MeshScalingDel2(Edge) = 1.0_Real;
o_MeshScalingDel4(Edge) = 1.0_Real;
});
} // if ScaleWithMesh

MeshScalingDel2H = createHostMirrorCopy(MeshScalingDel2);
MeshScalingDel4H = createHostMirrorCopy(MeshScalingDel4);
Expand Down
105 changes: 105 additions & 0 deletions components/omega/test/ocn/HorzMeshTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
#include "TimeMgr.h"
#include "mpi.h"

#include <algorithm>
#include <cmath>

using namespace OMEGA;

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -574,6 +577,108 @@ int main(int argc, char *argv[]) {
if (Count > 0)
ABORT_ERROR("HorzMeshTest: edgeSignOnVertex test FAIL");

// Test mesh scaling with scaling disabled
Count = 0;
for (int Edge = 0; Edge < Mesh->NEdgesAll; Edge++) {
if (abs(Mesh->MeshScalingDel2H(Edge) - 1.0) > Tol)
Count++;
if (abs(Mesh->MeshScalingDel4H(Edge) - 1.0) > Tol)
Count++;
}

if (Count > 0)
ABORT_ERROR("HorzMeshTest: disabled mesh scaling test FAIL");

// Retrieve mesh scaling configuration so the two MPAS-O scaling modes
// can be tested independently.
Config *OmegaConfig = Config::getOmegaConfig();
Config HorzMeshConfig("HorzMesh");
Error ConfigErr = OmegaConfig->get(HorzMeshConfig);
CHECK_ERROR_ABORT(ConfigErr,
"HorzMeshTest: HorzMesh group not found in Config");
Config MeshScalingConfig("MeshScaling");
ConfigErr = HorzMeshConfig.get(MeshScalingConfig);
CHECK_ERROR_ABORT(ConfigErr,
"HorzMeshTest: MeshScaling group not found in Config");

// Test scaling based on a configured reference cell width
const Real RefWidth = 30000.0_Real;
MeshScalingConfig.set("ScaleWithMesh", true);
MeshScalingConfig.set("UseRefWidth", true);
MeshScalingConfig.set("RefWidth", RefWidth);
Mesh->computeMeshScaling();

Count = 0;
for (int Edge = 0; Edge < Mesh->NEdgesAll; Edge++) {
const int Cell0 = Mesh->CellsOnEdgeH(Edge, 0);
const int Cell1 = Mesh->CellsOnEdgeH(Edge, 1);
const Real CellWidth =
2.0_Real *
std::sqrt((Mesh->AreaCellH(Cell0) + Mesh->AreaCellH(Cell1)) /
(2.0_Real * Pi));
const Real RefDel2 = CellWidth / RefWidth;
const Real RefDel4 = RefDel2 * RefDel2 * RefDel2;

if (abs(Mesh->MeshScalingDel2H(Edge) - RefDel2) >
Tol * std::max(1.0_Real, std::abs(RefDel2)) ||
abs(Mesh->MeshScalingDel4H(Edge) - RefDel4) >
Tol * std::max(1.0_Real, std::abs(RefDel4))) {
++Count;
}
}

if (Count > 0)
ABORT_ERROR("HorzMeshTest: reference-width mesh scaling test FAIL");

// Test legacy scaling based on mesh density. A negative configured
// maximum requests that the global maximum be computed from the mesh.
MeshScalingConfig.set("UseRefWidth", false);
MeshScalingConfig.set("MaxMeshDensity", -1.0);
Mesh->computeMeshScaling();

Real MaxMeshDensity;
ConfigErr = MeshScalingConfig.get("MaxMeshDensity", MaxMeshDensity);
CHECK_ERROR_ABORT(
ConfigErr,
"HorzMeshTest: unable to retrieve computed MaxMeshDensity");

Real RefMaxMeshDensityLocal = 0.0_Real;
for (int Cell = 0; Cell < LocCells; ++Cell) {
RefMaxMeshDensityLocal =
std::max(RefMaxMeshDensityLocal, Mesh->MeshDensityH(Cell));
}
Real RefMaxMeshDensity;
Err = MPI_Allreduce(&RefMaxMeshDensityLocal, &RefMaxMeshDensity, 1,
MPI_RealKind, MPI_MAX, Comm);
if (Err != MPI_SUCCESS)
ABORT_ERROR("HorzMeshTest: MPI error finding max mesh density");

if (abs(MaxMeshDensity - RefMaxMeshDensity) >
Tol * std::max(1.0_Real, std::abs(RefMaxMeshDensity))) {
ABORT_ERROR("HorzMeshTest: computed MaxMeshDensity test FAIL");
}

Count = 0;
for (int Edge = 0; Edge < Mesh->NEdgesAll; Edge++) {
const int Cell0 = Mesh->CellsOnEdgeH(Edge, 0);
const int Cell1 = Mesh->CellsOnEdgeH(Edge, 1);
const Real AvgDensity =
0.5_Real * (Mesh->MeshDensityH(Cell0) + Mesh->MeshDensityH(Cell1));
const Real DensityRatio = AvgDensity / RefMaxMeshDensity;
const Real RefDel2 = 1.0_Real / std::pow(DensityRatio, 0.25_Real);
const Real RefDel4 = 1.0_Real / std::pow(DensityRatio, 0.75_Real);

if (abs(Mesh->MeshScalingDel2H(Edge) - RefDel2) >
Tol * std::max(1.0_Real, std::abs(RefDel2)) ||
abs(Mesh->MeshScalingDel4H(Edge) - RefDel4) >
Tol * std::max(1.0_Real, std::abs(RefDel4))) {
++Count;
}
}

if (Count > 0)
ABORT_ERROR("HorzMeshTest: mesh-density scaling test FAIL");

// Test cell halo values
// Perform halo exhange on owned cell only array and compare
// read values
Expand Down
Loading