Add MOC stream function analysis capability - #481
Conversation
|
Sorry, @brian-oneill, I didn't get to this today. I'll try again tomorrow. Also, let me know what you need from me regarding both the dynamic streams here in Omega and the Polaris support. |
| ReductionPeriod: [1Month] | ||
| SnapshotPeriod: [1Day] |
There was a problem hiding this comment.
Can you help me understand what sets how often the MOC is computed? Is it the SnapshotPeriod? With the options above, ReductionPeriod would then be averaging daily instantaneous MOC values over 1 month?
There was a problem hiding this comment.
My understanding is that they are independent of one another. In practice, we would have:
ReductionPeriod: [1Month]
SnapshotPeriod: []
since we want monthly averages and don't need snapshots.
It's hard for me to imagine very much analysis where we want both time averages and snapshots at the same time, in practice.
There was a problem hiding this comment.
Yes, ReductionPeriod and SnapshotPeriod produce outputs independently. The ReductionPeriod outputs currently accumulate every timestep so the MOC is computed each timestep for time averages, but this can be extended to allow for a courser sampling frequency pretty easily.
| # Computes spatial reduction statistics (Mean, Min, Max, StdDev) | ||
| # for a set of ocean fields. Supports temporal reduction (time-averaged | ||
| # output over a window) and instantaneous snapshots (discrete sampling). | ||
| Fields: [NormalVelocity, PseudoThickness, Temperature, Salinity] |
There was a problem hiding this comment.
Do we need to test that when, e.g., LayerThickness_BinaryMultiply(NormalVelocity), is present here that the MOC chain uses the available field or is this kind of thing covered by existing CTests?
There was a problem hiding this comment.
Well, I guess not present here because GlobalStats reduces spatially.
There was a problem hiding this comment.
During the initial parsing, the parser checks if a Field that would be output by an operator has already been registered, to prevent building a duplicate operator. There is a unit test that checks this behavior, but it could be more robust.
xylar
left a comment
There was a problem hiding this comment.
@brian-oneill, I'll do some testing but here are a few comments to keep the process moving.
This looks great! A lot of the pieces are in place and just a few tweaks would be helpful, I think. Plus a few things that might be for now or might be postponed until later.
| ReductionPeriod: [1Month] | ||
| SnapshotPeriod: [1Day] |
There was a problem hiding this comment.
My understanding is that they are independent of one another. In practice, we would have:
ReductionPeriod: [1Month]
SnapshotPeriod: []
since we want monthly averages and don't need snapshots.
It's hard for me to imagine very much analysis where we want both time averages and snapshots at the same time, in practice.
There was a problem hiding this comment.
Could ScalarMultiply be generalized to take a model config option or known constant as its input, not just a hard-coded number? This would seem much more useful and general.
There was a problem hiding this comment.
For the use case here, this Op gets passed 1e-6 through a "config" that is programmatically defined in the parser. So 1e-6 is hard-coded into the MOC chain construction, but the Op itself is designed to take a configurable value to be compatible with the future composable framework.
There was a problem hiding this comment.
Sounds good. It's RhoSw in particular that I wanted to know about.
There was a problem hiding this comment.
Gotcha, that's possible but requires a little more work. Allowing users to request a defined constant from the config file would require defining a map between string labels and the variable names in GlobalConstants.h
std::map<std::string,Real> Constants = {
{"RhoSw", RhoSw},
{"Gravity", Gravity},
{"Pi", Pi},
...
};
There was a problem hiding this comment.
I think we should try to figure out how to automate that somehow but, yes, that's what I was anticipating. Nothing that needs to be in this PR.
There was a problem hiding this comment.
Yeah, automated would be best. Here's Claude's suggestion:
Yes, the X-macro pattern is exactly right. The idea: define a single list of constants once using a macro, then expand it in two ways — once to declare the constexpr values, and once to build the lookup.
In GlobalConstants.h, replace the individual constexpr declarations for the physical constants with:
// X-macro list: X(Name, Value)
#define OMEGA_PHYSICAL_CONSTANTS(X) \
X(RhoSw, pcd::seawater_density_reference) \
X(RhoFw, pcd::pure_water_density_reference) \
X(RhoAir, pcd::dry_air_density_at_standard_temperature_and_pressure) \
X(Gravity, pcd::standard_acceleration_of_gravity) \
X(RhoIce, pcd::sea_ice_density_reference) \
/* ... all others ... */
// Expand to constexpr declarations (same as before)
#define DECLARE_CONST(Name, Value) constexpr Real Name = (Value);
OMEGA_PHYSICAL_CONSTANTS(DECLARE_CONST)
#undef DECLARE_CONSTThen the lookup function writes itself:
inline std::optional<Real> getConstantByName(const std::string &Name) {
#define MATCH_CONST(CName, Value) if (Name == #CName) return CName;
OMEGA_PHYSICAL_CONSTANTS(MATCH_CONST)
#undef MATCH_CONST
return std::nullopt;
}#CName stringifies the identifier automatically, so the name in the config file ("RhoSw") matches the C++ variable name without any manual duplication.
Pros: Zero maintenance burden — adding a new constant to the list automatically makes it available by name to ScalarMultiplyOp and any future operator that calls getConstantByName.
Cons: Requires refactoring the existing constexpr declarations in GlobalConstants.h into the macro list format. The math-derived ones (TwoPi, SDay, etc.) are trickier since they depend on other constants — those would need to stay as regular constexpr or be added after the macro expansion.
The practical approach: put only the "leaf" physical constants (densities, heat capacities, etc.) in the X-macro list, and keep derived/compound ones (TwoPi, TkFrzSw, SDay) as regular constexpr declarations below. You could optionally add those to a second X-macro list if you want them accessible by name too.
Allow for an optional boundary condition and add a 1D compute method
Remove horizontal dimension assertion and some refactoring
There was a problem hiding this comment.
I successfully ran a 5-day test with the MOC on in:
/lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260804/ec30to60-global-moc/ocean/spherical/realistic_global/EC30to60E2r2/analysis_members_test
I used:
MOC:
Enable: true
# Meridional Overturning Circulation (MOC) streamfunction analysis group
# Computes MOC as a function of latitude and depth for regions,
# and as a function of depth for transects
NumBins: 180 # Number of latitude bins (default: 180, ~1 degree)
MinLat: -90.0 # Minimum latitude in degrees (default: -90.0)
MaxLat: 90.0 # Maximum latitude in degrees (default: 90.0)
Regions: [Global] # List of region names for regional MOC
# NOTE: Region masks not yet implemented
Transects: [] # List of transect names for transect-based MOC
# NOTE: Transect masks not yet implemented
ReductionPeriod: [1day] # Temporal reduction periods
SnapshotPeriod: [] # Instantaneous output periods
Filename: moc.$Y
Stream:
FileFreq: 1
FileFreqUnits: daysSo daily averaging rather than monthly for efficiency.
However, the latitude bins and depth are missing from the output file:
$ ncdump -h moc_1dayTimeStats.0001
netcdf moc_1dayTimeStats {
dimensions:
MaxCellsOnEdge = 2 ;
MaxEdges = 7 ;
MaxEdges2 = 14 ;
NCells = 236853 ;
NEdges = 719506 ;
NTracers = 2 ;
NVertLayers = 60 ;
NVertLayersP1 = 61 ;
NVertices = 482371 ;
NumBinsLatCell_BinIndex = 180 ;
Scalar = 1 ;
VertexDegree = 3 ;
time = UNLIMITED ; // (5 currently)
variables:
double MOC_streamfunction_Global_TimeMean1day(time, NumBinsLatCell_BinIndex, NVertLayersP1) ;
MOC_streamfunction_Global_TimeMean1day:Description = "Time average of VerticalPseudoVelocity_PseudoToGeometric_BinaryMultiply(AreaCell)_BinnedAccumulator(LatCell_BinIndex)_PrefixSum_ScalarMultiply(1.0e-6)" ;
MOC_streamfunction_Global_TimeMean1day:Name = "VerticalPseudoVelocity_PseudoToGeometric_BinaryMultiply(AreaCell)_BinnedAccumulator(LatCell_BinIndex)_PrefixSum_ScalarMultiply(1.0e-6)_TimeMean1day" ;
MOC_streamfunction_Global_TimeMean1day:StdName = "" ;
MOC_streamfunction_Global_TimeMean1day:Units = "" ;
MOC_streamfunction_Global_TimeMean1day:ValidMax = 1.79769313486232e+308 ;
MOC_streamfunction_Global_TimeMean1day:ValidMin = -1.79769313486232e+308 ;
MOC_streamfunction_Global_TimeMean1day:_FillValue = 9.96920996838687e+36 ;
MOC_streamfunction_Global_TimeMean1day:long_name = "Time average of VerticalPseudoVelocity_PseudoToGeometric_BinaryMultiply(AreaCell)_BinnedAccumulator(LatCell_BinIndex)_PrefixSum_ScalarMultiply(1.0e-6)" ;
MOC_streamfunction_Global_TimeMean1day:name = "VerticalPseudoVelocity_PseudoToGeometric_BinaryMultiply(AreaCell)_BinnedAccumulator(LatCell_BinIndex)_PrefixSum_ScalarMultiply(1.0e-6)_TimeMean1day" ;
MOC_streamfunction_Global_TimeMean1day:standard_name = "" ;
MOC_streamfunction_Global_TimeMean1day:units = "" ;
MOC_streamfunction_Global_TimeMean1day:valid_max = 1.79769313486232e+308 ;
MOC_streamfunction_Global_TimeMean1day:valid_min = -1.79769313486232e+308 ;
double time(time) ;
time:Description = "time" ;
time:Name = "time" ;
time:StdName = "time" ;
time:Units = "seconds since 0001-01-01 00:00:00" ;
time:ValidMax = 1.e+20 ;
time:ValidMin = 0. ;
time:_FillValue = 9.96920996838687e+36 ;
time:calendar = "noleap" ;
time:long_name = "time" ;
time:name = "time" ;
time:standard_name = "time" ;
time:units = "seconds since 0001-01-01 00:00:00" ;
time:valid_max = 1.e+20 ;
time:valid_min = 0. ;
// global attributes:
:SimulationTime = "0001-01-06_00:00:00" ;
:SimulationTime0 = "0001-01-02_00:00:00" ;
:SimulationTime1 = "0001-01-03_00:00:00" ;
:SimulationTime2 = "0001-01-04_00:00:00" ;
:SimulationTime3 = "0001-01-05_00:00:00" ;
:SimulationTime4 = "0001-01-06_00:00:00" ;
}
Also the file is missing a .nc extension.
Happy to rerun once this is fixed.
|
Is I've noticed the history and hifreq files that get created when running the omega ctests also no longer get the |
|
The problem with the current layer-wise approach is it implicitly assumes pure z-level layers. The best way to get depths given this approach is to get the area-weighted average of zInterface and use that as the vertical coordinate. For now, that's fine. With ice-shelf cavities, sigma coordinates, etc. in the future, we'll need to do vertical binning or interpolation to a z-level or density-level grid. |
|
I'm fine with whatever fix to the |
…streamfunction' into omega/analysis-moc-streamfunction
| # List of field names to compute statistics for | ||
| SpatialStats: [Max, Min, Mean, StdDev] | ||
| # Spatial statistics to compute (one per field) | ||
| ReductionPeriod: [1Day, 1Month] |
There was a problem hiding this comment.
| ReductionPeriod: [1Day, 1Month] | |
| ReductionPeriod: [] |
Should we remove reductions altogether from this analysis member to save compute time?
There was a problem hiding this comment.
I think we want 1Month reduction for the climatology plot, don't we?
There was a problem hiding this comment.
I guess it depends on what configuration Default.yml is aimed at -- typical standalone testing or a longer production run.
| # Spatial statistics to compute (one per field) | ||
| ReductionPeriod: [1Day, 1Month] | ||
| # Temporal reduction periods (time-averaged stats) | ||
| SnapshotPeriod: [6Hours] |
There was a problem hiding this comment.
MPAS-O is equivalent to 1Day but I like increasing the freq to 6 hours
@brian-oneill Is this ready for testing with the fix to this comment? |
@cbegeman I added the latitude bins. The depths will require a bit more thought and work, and I won’t have time to complete that piece until I’m back at the end of the week. |

Overview
This PR introduces a complete MOC (Meridional Overturning Circulation) analysis capability to Omega, enabling computation of the MOC streamfunction using two complementary methods:
The implementation adds 8 new analysis operators, enhanced analysis infrastructure with regional mask support, a new MOC analysis group, and comprehensive configuration templates.
Key Features
New Analysis Operators (8 total):
BinaryMultiplyOp: Element-wise field multiplication with vertical expansion supportBinnedAccumulatorOp: Accumulates field values into spatial bins (core MOC operator)CoordinateBinningOp: Assigns mesh entities to bins based on coordinate valuesExtractRegionOp: Applies regional masks to fieldsPrefixSumOp: Cumulative summation (integration) along specified dimensionPseudoToGeometricOp: Converts pseudo-height quantities to geometric coordinatesScalarMultiplyOp: Multiplies field by scalar constant for unit conversionTransectAccumulatorOp: Accumulates transport across transect edgesInfrastructure Enhancements:
setOutputIONamemethod and operator-specific configuration supportMOC Analysis Group:
MOC Computation Pipeline
Latitude-binned Regional MOC chain:
Transect-based MOC chain:
Technical Implementation
Design Features:
Output Format:
Limitations
Checklist
Documentation:
Linting
Building
Testing
aurora, oneapi-ifx, mpich
chrysalis, oneapi-ifx, openmpi
frontier, craygnu, mpich
frontier, craygnu-mphipcc, mpich
pm-cpu, gnu, mpich
pm-gpu, gnugpu, mpich
Provide relevant details in a comment to the PR titled
Testingwith the following:have been run on and indicate that are all passing.
has passed, using the Polaris
e3sm_submodules/Omegabaseline-pfor both the baseline (Polarise3sm_submodules/Omega) and the PR buildNew tests: