diff --git a/components/omega/doc/devGuide/Analysis.md b/components/omega/doc/devGuide/Analysis.md
index 6199fb3b7bfb..d369d11fb0a6 100644
--- a/components/omega/doc/devGuide/Analysis.md
+++ b/components/omega/doc/devGuide/Analysis.md
@@ -144,7 +144,9 @@ A concrete derived operator is responsible for three things:
record the input Field names, construct the output Field name(s), allocate
the output data array, and register the output [Field](#omega-dev-field)
with the appropriate metadata. Output Field names follow the convention
- `_` (for example, `Temperature_SpatialMax`).
+ `_` (for example, `Temperature_SpatialMax`). The
+ output's CF metadata is derived from the input's, as described under
+ [Output metadata](#omega-dev-analysis-output-metadata).
2. **Initialization**: `initialize()` is called by the orchestrator after all
Fields exist. The base implementation stores the mesh, vertical
coordinate, and communicator; operators may override to perform additional
@@ -245,6 +247,69 @@ normalization. The sums accumulate each owned entity's terms in a fixed
order and reduce them with `globalSum`, so they are as reproducible as
Omega's other global sums.
+(omega-dev-analysis-output-metadata)=
+### Output metadata
+
+An output Field inherits its CF metadata from the input it is derived from.
+The base class provides the helpers that do this:
+
+- `inheritMetadata(InputName, CellMethod)` returns an `InheritedMetadata`
+ struct holding the input's `units`, `standard_name` and `cell_methods`,
+ each empty if the input has none, with `CellMethod` appended to the cell
+ methods. CF requires `cell_methods` to list the reductions in the order
+ they were applied, so a time mean of a spatial mean carries
+ `area: depth: mean time: mean`.
+- `spatialCellMethod(InputName, Method)` returns the cell method for a
+ reduction over all owned mesh entities and layers of the input:
+ `area: ` for a horizontal (1D) field and `area: depth: `
+ for a field with a vertical dimension. `Method` is a CF cell method name
+ (`mean`, `minimum`, `maximum`, `standard_deviation`, `sum`).
+- `createOutputField(...)` calls `Field::create()` with the inherited units
+ and standard name and adds the `cell_methods` attribute when it is not
+ empty.
+- `getFieldAttribute(FieldName, AttName)` returns any string attribute of a
+ Field, or an empty string if the Field has none.
+
+The built-in reductions (mean, minimum, maximum, standard deviation, time
+mean) all have the units and standard name of the field they reduce, so
+they pass `inheritMetadata()` straight to `createOutputField()`:
+```c++
+auto CellMethod = spatialCellMethod(InputNames[0], "mean");
+auto Meta = inheritMetadata(InputNames[0], CellMethod);
+auto OutputField = createOutputField(OutputNames[0],
+ "Spatial mean of " + InputNames[0],
+ Meta, ValidMin, ValidMax, NDims, DimNames);
+```
+An operator whose result has different units derives them with
+[CFUnits](#omega-dev-cfunits) before creating the output, and clears the
+standard name if the result is no longer the same physical quantity. For
+example, an area-weighted sum multiplies the input's units by those of the
+cell area:
+```c++
+auto Meta = inheritMetadata(InputNames[0],
+ spatialCellMethod(InputNames[0], "sum"));
+Meta.Units = CFUnits::multiply(Meta.Units, "m2");
+Meta.StdName.clear();
+```
+The other operators derive their units the same way:
+
+- `BinaryMultiply` multiplies the units of its two inputs (a velocity in
+ `m s-1` times an area in `m2` is `m3 s-1`) and has no standard name or cell
+ methods, since the product is a new quantity.
+- `ScalarMultiply` scales the units by the reciprocal of its scalar with
+ `CFUnits::scale()`, so a flux in `m3 s-1` multiplied by `1e-6` is in
+ `1e+06 m3 s-1` (Sverdrups). A negative scalar also clears the standard
+ name, since it reverses the quantity.
+- `HorzMean` inherits the units and standard name and appends `area: mean`.
+- `BinnedAccumulator`, `TransectAccumulator` and `PrefixSum` sum their input,
+ and `ExtractRegion` and `PseudoToGeometric` multiply it by a mask or a
+ dimensionless factor, so all of them inherit its units. `PrefixSum` aborts
+ if its boundary condition field has different units from its input.
+- `CoordinateBinning` outputs a bin index, which is dimensionless (`1`).
+
+A field without units yields outputs without units, never with invented ones,
+and a malformed units string aborts the run when the operator is constructed.
+
## Operator factory and type dispatch
Fields carry runtime metadata describing their scalar type, rank, and memory
@@ -493,9 +558,11 @@ To add a new operator:
operator type name to the base constructor, set `InputNames`, build the
output name as `_`, set `OutputNames` and `InstanceName`,
allocate the output data array, and register the output Field with
- `Field::create()` plus `attachData()`. If the operator depends on another
- operator's output (as `SpatialStdDev` depends on `SpatialMean`), append
- that dependency to `InputNames`.
+ `createOutputField()` plus `attachData()`, deriving its units, standard
+ name and cell methods from the input as described under
+ [Output metadata](#omega-dev-analysis-output-metadata). If the operator
+ depends on another operator's output (as `SpatialStdDev` depends on
+ `SpatialMean`), append that dependency to `InputNames`.
2. **Implement `compute()`** to retrieve the input array(s) from the Field
registry, perform the transformation, write the output array(s), and update
`LastComputed`/`FieldComputed`.
diff --git a/components/omega/doc/devGuide/CFUnits.md b/components/omega/doc/devGuide/CFUnits.md
new file mode 100644
index 000000000000..407ad51de2f3
--- /dev/null
+++ b/components/omega/doc/devGuide/CFUnits.md
@@ -0,0 +1,58 @@
+(omega-dev-cfunits)=
+
+## CFUnits
+
+Omega fields carry their units as a string attribute in the plain form used
+by the CF standard name table: unit symbols separated by single spaces, each
+with an optional signed integer exponent (`m s-1`, `kg m-3`, `W m-2`), and `1`
+for a dimensionless quantity. When a field is derived from others, its units
+must be derived too. The `CFUnits` class parses such a string, supports the
+product, quotient and integer power of units expressions, and formats the
+result back in the plain form.
+
+The string interfaces cover the common cases:
+```c++
+#include "CFUnits.h"
+
+std::string Flux = CFUnits::multiply("m s-1", "m2"); // "m3 s-1"
+std::string Speed = CFUnits::divide("m3 s-1", "m2"); // "m s-1"
+std::string Energy = CFUnits::power("m s-1", 2); // "m2 s-2"
+```
+Exponents of the same symbol are merged (`m s-1` times `m` is `m2 s-1`) and
+a symbol whose exponent reaches zero is dropped, so `kg m-3` times `m3` is
+`kg`. In the result, symbols with positive exponents come first, then those
+with negative exponents, each in the order they first appeared.
+
+A units string may also start with a positive number, the scale factor of a
+scaled unit as udunits writes it: `1e+06 m3 s-1` is a million cubic meters
+per second. Multiplying a field's values by a constant, for example to report
+a transport in units of 10^6 m3 s-1, scales its units by the reciprocal:
+```c++
+std::string Transport = CFUnits::scale("m3 s-1", 1.0 / 1.0e-6); // "1e+06 m3 s-1"
+```
+Scale factors multiply through products, quotients and powers, and are
+written with the fewest digits that reproduce them, so the rounding error in
+`1.0 / 1.0e-6` does not show. A number is only accepted before the unit
+symbols, and must be positive and finite.
+
+A field with no units attribute has *unknown* units, represented by the empty
+string. Unknown units propagate through every operation, so a field derived
+from one without units has no units either, rather than wrong ones.
+
+The parser also accepts the udunits caret and slash spellings (`m^2`, `m/s`),
+which some older field definitions still use, but always emits the plain
+form. Any other string is an error: the string interfaces abort, and the
+value interface reports it so a caller can decide:
+```c++
+CFUnits Units;
+Error Err = CFUnits::parse("m s-1", Units); // Fail for a malformed string
+CFUnits Area;
+CFUnits::parse("m2", Area);
+std::string Flux = (Units * Area).str(); // "m3 s-1"
+```
+`CFUnits` also provides `operator/`, `pow(int)`, `scaled(double)`,
+`isUnknown()`, `isDimensionless()` and an order-insensitive `operator==`.
+
+A malformed units string is a programming error in a field definition, and
+aborting when the derived field is created stops the run at initialization
+rather than writing output with a wrong or missing attribute.
diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md
index b16840d3bc5c..a7f78ede86c3 100644
--- a/components/omega/doc/index.md
+++ b/components/omega/doc/index.md
@@ -84,6 +84,7 @@ devGuide/Decomp
devGuide/Dimension
devGuide/Error
devGuide/Field
+devGuide/CFUnits
devGuide/IO
devGuide/IOStreams
devGuide/Halo
diff --git a/components/omega/doc/userGuide/Analysis.md b/components/omega/doc/userGuide/Analysis.md
index e760f15b3e5f..945a3f555413 100644
--- a/components/omega/doc/userGuide/Analysis.md
+++ b/components/omega/doc/userGuide/Analysis.md
@@ -101,7 +101,11 @@ user-specified fields. The mean and standard deviation are weighted so that
they do not depend on the mesh resolution: by the area of each cell, edge or
vertex for a horizontal field, and by mass (area times pseudo-thickness) for
a field with a vertical dimension, with an interface field weighted by half
-the mass of each adjacent layer. Only active layers take part.
+the mass of each adjacent layer. Only active layers take part. Each statistic
+is written with the `units` and `standard_name` of the field it reduces and a
+CF `cell_methods` attribute describing the reduction, for example
+`area: depth: mean` for the mean of a layered field and
+`area: depth: mean time: mean` for its time mean.
**Example:**
@@ -157,7 +161,9 @@ Computes the Meridional Overturning Circulation (MOC) streamfunction using two
methods: latitude-binned regional MOC and transect-based MOC. The MOC
represents zonally integrated meridional mass transport as a function of
latitude and depth (regional MOC) or depth alone (transect MOC). Output is in
-Sverdrups (Sv), where 1 Sv = 10⁶ m³/s.
+Sverdrups (Sv), where 1 Sv = 10⁶ m³/s, so its `units` attribute is
+`1e+06 m3 s-1`, the udunits spelling of a Sverdrup (udunits reads `Sv` as the
+sievert).
**Example:**
diff --git a/components/omega/src/analysis/AnalysisOperator.cpp b/components/omega/src/analysis/AnalysisOperator.cpp
index 59a0a44d4b29..3425dc592530 100644
--- a/components/omega/src/analysis/AnalysisOperator.cpp
+++ b/components/omega/src/analysis/AnalysisOperator.cpp
@@ -87,6 +87,86 @@ const std::vector AnalysisOperator::getOutputFieldNames() {
return OutputNames;
} // end getOutputFieldNames
+//------------------------------------------------------------------------------
+// Returns a string attribute of a Field, or an empty string if the Field has
+// no such attribute. Analysis output is derived from fields that may lack
+// units or a standard name, so absence is the empty string, not an error.
+std::string AnalysisOperator::getFieldAttribute(const std::string &FieldName,
+ const std::string &AttName) {
+
+ auto FieldPtr = Field::get(FieldName);
+ OMEGA_REQUIRE(FieldPtr, "AnalysisOperator: field {} not found", FieldName);
+
+ std::string Value;
+ if (FieldPtr->hasMetadata(AttName)) {
+ Error Err = FieldPtr->getMetadata(AttName, Value);
+ CHECK_ERROR_ABORT(Err,
+ "AnalysisOperator: attribute {} of field {} is "
+ "not a string",
+ AttName, FieldName);
+ }
+ return Value;
+} // end getFieldAttribute
+
+//------------------------------------------------------------------------------
+// Collects the units, standard name and cell methods an output inherits from
+// its input. A new cell method is appended to the input's cell_methods so the
+// attribute lists the reductions in the order they were applied, as CF
+// requires (e.g. a time mean of a spatial mean is "area: mean time: mean").
+AnalysisOperator::InheritedMetadata
+AnalysisOperator::inheritMetadata(const std::string &InputName,
+ const std::string &CellMethod) {
+
+ InheritedMetadata Meta;
+ Meta.Units = getFieldAttribute(InputName, "units");
+ Meta.StdName = getFieldAttribute(InputName, "standard_name");
+ Meta.CellMethods = getFieldAttribute(InputName, "cell_methods");
+
+ if (!CellMethod.empty()) {
+ if (!Meta.CellMethods.empty())
+ Meta.CellMethods += " ";
+ Meta.CellMethods += CellMethod;
+ }
+ return Meta;
+} // end inheritMetadata
+
+//------------------------------------------------------------------------------
+// Returns the cell method for a reduction over all owned mesh entities and
+// layers of an input Field. The spatial operators treat a 1D field as
+// horizontal only and the last dimension of any higher-rank field as
+// vertical, so the method covers "area" alone or "area" and "depth". Both
+// are names CF allows in cell_methods without a matching coordinate.
+std::string AnalysisOperator::spatialCellMethod(const std::string &InputName,
+ const std::string &Method) {
+
+ auto FieldPtr = Field::get(InputName);
+ OMEGA_REQUIRE(FieldPtr, "AnalysisOperator: field {} not found", InputName);
+
+ if (FieldPtr->getNumDims() >= 2)
+ return "area: depth: " + Method;
+ return "area: " + Method;
+} // end spatialCellMethod
+
+//------------------------------------------------------------------------------
+// Creates an output Field carrying the inherited CF metadata. The cell
+// methods are stored as a std::string so the metadata can be retrieved and
+// written as a string attribute.
+std::shared_ptr AnalysisOperator::createOutputField(
+ const std::string &OutputName, const std::string &Description,
+ const InheritedMetadata &Meta, const std::any ValidMin,
+ const std::any ValidMax, const int NumDims,
+ const std::vector &DimNames) {
+
+ auto OutputField =
+ Field::create(OutputName, Description, Meta.Units, Meta.StdName,
+ ValidMin, ValidMax, NumDims, DimNames);
+
+ if (!Meta.CellMethods.empty())
+ OutputField->addMetadata("cell_methods", Meta.CellMethods);
+
+ return OutputField;
+} // end createOutputField
+
//------------------------------------------------------------------------------
// Checks whether the operator's output is valid for the given timestamp.
// Returns true if the operator has been computed and the LastComputed
diff --git a/components/omega/src/analysis/AnalysisOperator.h b/components/omega/src/analysis/AnalysisOperator.h
index aa3800c7090a..ee708255e3bb 100644
--- a/components/omega/src/analysis/AnalysisOperator.h
+++ b/components/omega/src/analysis/AnalysisOperator.h
@@ -20,6 +20,13 @@
/// data from the Field registry and writes results to operator-owned output
/// arrays.
///
+/// An output Field inherits its CF metadata from the input it is derived
+/// from: the base class provides helpers that collect an input's units,
+/// standard name and cell methods, append the cell method describing the new
+/// reduction, and create the output Field with them. Operators whose result
+/// has different units (an area-weighted sum, a product) derive them with
+/// CFUnits before creating the output Field.
+///
/// This file also provides helper functions (opParam, makeOpConfig) for
/// constructing Config objects inline when instantiating operators,
/// providing a uniform parameter-passing mechanism whether operators are
@@ -27,6 +34,7 @@
///
//===----------------------------------------------------------------------===//
+#include "CFUnits.h"
#include "Config.h"
#include "DataTypes.h"
#include "Dimension.h"
@@ -165,6 +173,58 @@ class AnalysisOperator {
);
protected:
+ /// CF metadata an output Field inherits from the input it is derived
+ /// from. Units and standard name are copied and are empty when the input
+ /// has none. CellMethods records every reduction applied so far, in
+ /// order, as the CF cell_methods attribute (e.g. "area: mean time: mean").
+ struct InheritedMetadata {
+ std::string Units; ///< units of the input, empty if unknown
+ std::string StdName; ///< CF standard name, empty if none
+ std::string CellMethods; ///< CF cell_methods, empty if none
+ };
+
+ /// Collects the CF metadata an output Field inherits from an input Field,
+ /// with the cell method of the new reduction (if any) appended to those
+ /// the input already records. The units and standard name are copied
+ /// unchanged; an operator that changes the units or invalidates the
+ /// standard name edits the result before creating its output Field.
+ static InheritedMetadata
+ inheritMetadata(const std::string &InputName, ///< [in] input field name
+ const std::string &CellMethod = "" ///< [in] new reduction
+ );
+
+ /// Returns the CF cell method for a spatial reduction of an input Field
+ /// over all of its owned mesh entities and layers: "area: " for a
+ /// horizontal field and "area: depth: " for a field with a
+ /// vertical dimension. Method is a CF cell method name (mean, minimum,
+ /// maximum, standard_deviation, sum).
+ static std::string
+ spatialCellMethod(const std::string &InputName, ///< [in] input field name
+ const std::string &Method ///< [in] CF cell method
+ );
+
+ /// Returns the value of a string attribute (units, standard_name,
+ /// cell_methods, long_name) of a Field, or an empty string if the Field
+ /// has no such attribute
+ static std::string
+ getFieldAttribute(const std::string &FieldName, ///< [in] field name
+ const std::string &AttName ///< [in] attribute name
+ );
+
+ /// Creates an output Field with the given description, valid range and
+ /// dimensions, carrying the inherited units, standard name and cell
+ /// methods. Empty units or standard name are passed through to
+ /// Field::create; an empty cell_methods is not added.
+ static std::shared_ptr createOutputField(
+ const std::string &OutputName, ///< [in] output field name
+ const std::string &Description, ///< [in] long name of the output
+ const InheritedMetadata &Meta, ///< [in] inherited CF metadata
+ const std::any ValidMin, ///< [in] min valid value
+ const std::any ValidMax, ///< [in] max valid value
+ const int NumDims, ///< [in] number of dimensions
+ const std::vector &DimNames ///< [in] dimension names
+ );
+
const HorzMesh *Mesh; ///< Horizontal mesh for spatial operations
const VertCoord *VCoord; ///< Vertical coordinate for vertical ops
MPI_Comm Comm; ///< MPI communicator for parallel reductions
diff --git a/components/omega/src/analysis/analysisGroups/MOC.cpp b/components/omega/src/analysis/analysisGroups/MOC.cpp
index 2467cec6bf10..676728cdcb51 100644
--- a/components/omega/src/analysis/analysisGroups/MOC.cpp
+++ b/components/omega/src/analysis/analysisGroups/MOC.cpp
@@ -121,9 +121,9 @@ MOC::MOC(const std::string &GroupName, Config &AnalysisGroupOptions,
buildTemporalChains(ChainStems, AnalysisGroupOptions, AnalysisManager,
ChainConfigs);
- // Stamp Sverdrup units and a readable long_name on every streamfunction
- // output field. Both the CF-compliant "units" and the legacy capitalized
- // "Units" are set.
+ // Stamp a readable long_name on every streamfunction output field. The
+ // operators derive the units, 1e+06 m3 s-1 (Sverdrups) after the final
+ // ScalarMultiply(1e-6); "Sv" would be read by udunits as the sievert.
for (size_t i = 0; i < ChainStems.size(); ++i) {
const auto &StemStr = ChainStems[i];
const auto &RegionName = RegionList[i];
@@ -137,8 +137,6 @@ MOC::MOC(const std::string &GroupName, Config &AnalysisGroupOptions,
for (const auto &SFName : SFFieldNames) {
if (Field::exists(SFName)) {
auto SFField = Field::get(SFName);
- SFField->updateMetadata("units", std::string("Sv"));
- SFField->updateMetadata("Units", std::string("Sv"));
SFField->updateMetadata("long_name", LongName);
}
}
@@ -460,15 +458,14 @@ std::string MOC::buildDepthCoordChain(const std::string &RegionName,
// intermediate fields shared with other chains).
AnalysisManager->parseChainAndBuildOps(ChainStr, Cfg);
- // Attach descriptive depth metadata for output (once per unique field).
+ // Attach a descriptive long_name for output (once per unique field). The
+ // HorzMean operator derives the units from GeomZInterface.
if (Field::exists(DepthFieldName)) {
auto DepthField = Field::get(DepthFieldName);
DepthField->updateMetadata(
"long_name",
std::string("Area-weighted representative interface depth (") +
RegionName + ")");
- DepthField->updateMetadata("units", std::string("m"));
- DepthField->updateMetadata("Units", std::string("m"));
}
return DepthFieldName;
diff --git a/components/omega/src/analysis/operators/BinaryMultiplyOp.h b/components/omega/src/analysis/operators/BinaryMultiplyOp.h
index f27d337c8d09..0d923ca54fdf 100644
--- a/components/omega/src/analysis/operators/BinaryMultiplyOp.h
+++ b/components/omega/src/analysis/operators/BinaryMultiplyOp.h
@@ -139,34 +139,29 @@ template class BinaryMultiplyOp : public AnalysisOperator {
OutputNames = {OutputFieldName};
InstanceName = OutputFieldName;
- // Get metadata from both input fields
- std::string Descr1, Units1, StdName1;
- std::string Descr2, Units2, StdName2;
-
+ // Get descriptions from both input fields
+ std::string Descr1, Descr2;
Field1->getMetadata("Description", Descr1);
- Field1->getMetadata("Units", Units1);
- Field1->getMetadata("StdName", StdName1);
-
Field2->getMetadata("Description", Descr2);
- Field2->getMetadata("Units", Units2);
- Field2->getMetadata("StdName", StdName2);
+ std::string OutputDescr = "Product of " + Descr1 + " and " + Descr2;
- // Combine metadata for output
- std::string OutputDescr = "Product of " + Descr1 + " and " + Descr2;
- std::string OutputUnits = combineUnits(Units1, Units2);
- std::string OutputStdName = ""; // No standard name for generic product
+ // A product has the product of the input units (m s-1 times m2 is
+ // m3 s-1), which are unknown if either input's are. It is a new
+ // quantity, so it has no standard name or cell methods.
+ InheritedMetadata Meta;
+ Meta.Units = CFUnits::multiply(getFieldAttribute(InputNames[0], "units"),
+ getFieldAttribute(InputNames[1], "units"));
// Create output Field
- auto OutputField =
- Field::create(OutputNames[0],
- OutputDescr, // Description
- OutputUnits, // Units (combined)
- OutputStdName, // Standard name
- -std::numeric_limits::max(), // Min valid value
- std::numeric_limits::max(), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
- );
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ OutputDescr, // Description
+ Meta, // CF metadata
+ -std::numeric_limits::max(), // Min valid value
+ std::numeric_limits::max(), // Max valid value
+ NDims, // Rank
+ DimNames // Dimension names
+ );
// Allocate output data array matching input layout
OutputData = OutputArrayT(OutputNames[0] + "_out", Data1.layout());
@@ -379,21 +374,6 @@ template class BinaryMultiplyOp : public AnalysisOperator {
} // end compute
private:
- /// Combines units from two fields for the product
- std::string combineUnits(const std::string &Units1,
- const std::string &Units2) {
- if (Units1.empty() && Units2.empty()) {
- return "";
- } else if (Units1.empty()) {
- return Units2;
- } else if (Units2.empty()) {
- return Units1;
- } else {
- // Combine with multiplication notation
- return Units1 + " * " + Units2;
- }
- }
-
/// Output data array holding the product field values
OutputArrayT OutputData;
diff --git a/components/omega/src/analysis/operators/BinnedAccumulatorOp.h b/components/omega/src/analysis/operators/BinnedAccumulatorOp.h
index b55582eeea80..0f413ffce36a 100644
--- a/components/omega/src/analysis/operators/BinnedAccumulatorOp.h
+++ b/components/omega/src/analysis/operators/BinnedAccumulatorOp.h
@@ -125,14 +125,14 @@ template class BinnedAccumulatorOp : public AnalysisOperator {
OutputNames = {OutputFieldName};
InstanceName = OutputFieldName;
- // Get input metadata
- std::string ValueDescr, ValueUnits, ValueStdName;
+ // Get input metadata. A sum over the entities in each bin has the
+ // units of the values summed.
+ std::string ValueDescr;
ScalarT ValueValidMin, ValueValidMax;
ValueField->getMetadata("Description", ValueDescr);
- ValueField->getMetadata("Units", ValueUnits);
- ValueField->getMetadata("StdName", ValueStdName);
ValueField->getMetadata("ValidMin", ValueValidMin);
ValueField->getMetadata("ValidMax", ValueValidMax);
+ auto Meta = inheritMetadata(InputNames[0]);
// Create output Field dimensions
std::string NumBinsDimName = "NumBins" + InputNames[1];
@@ -147,16 +147,15 @@ template class BinnedAccumulatorOp : public AnalysisOperator {
}
// Create output Field
- auto OutputField =
- Field::create(OutputNames[0],
- "Binned accumulation of " + ValueDescr, // Description
- ValueUnits, // Units
- ValueStdName, // Standard name
- ValueValidMin, // Min valid
- ValueValidMax, // Max valid
- InputRank, // Rank
- OutputDimNames // Dimension names
- );
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ "Binned accumulation of " + ValueDescr, // Description
+ Meta, // CF metadata
+ ValueValidMin, // Min valid
+ ValueValidMax, // Max valid
+ InputRank, // Rank
+ OutputDimNames // Dimension names
+ );
// Allocate output and local accumulation arrays
if (InputRank == 1) {
diff --git a/components/omega/src/analysis/operators/ExtractRegionOp.h b/components/omega/src/analysis/operators/ExtractRegionOp.h
index 191c540e4ba4..d4ef6fa4c615 100644
--- a/components/omega/src/analysis/operators/ExtractRegionOp.h
+++ b/components/omega/src/analysis/operators/ExtractRegionOp.h
@@ -155,26 +155,25 @@ template class ExtractRegionOp : public AnalysisOperator {
std::vector DimNames;
InputField->getDimNames(DimNames);
- // Get input metadata
- std::string InputDescr, InputUnits, InputStdName;
+ // Get input metadata. Masking a region leaves the units, standard name
+ // and cell methods unchanged.
+ std::string InputDescr;
ScalarT InputValidMin, InputValidMax;
InputField->getMetadata("Description", InputDescr);
- InputField->getMetadata("Units", InputUnits);
- InputField->getMetadata("StdName", InputStdName);
InputField->getMetadata("ValidMin", InputValidMin);
InputField->getMetadata("ValidMax", InputValidMax);
+ auto Meta = inheritMetadata(InputNames[0]);
// Create output Field with same dimensions as input
- auto OutputField = Field::create(
+ auto OutputField = createOutputField(
OutputNames[0],
InputDescr + " (region: " + RegionName + ")", // Description
- InputUnits, // Units (unchanged)
- InputStdName, // Standard name (unchanged)
- InputValidMin, // Min valid (unchanged)
- InputValidMax, // Max valid (unchanged)
- NDims, // Rank
- DimNames // Dimension names
+ Meta, // CF metadata
+ InputValidMin, // Min valid (unchanged)
+ InputValidMax, // Max valid (unchanged)
+ NDims, // Rank
+ DimNames // Dimension names
);
// Allocate output data array matching input layout
diff --git a/components/omega/src/analysis/operators/HorzMeanOp.h b/components/omega/src/analysis/operators/HorzMeanOp.h
index c438b4f14834..a2a9754a1eb3 100644
--- a/components/omega/src/analysis/operators/HorzMeanOp.h
+++ b/components/omega/src/analysis/operators/HorzMeanOp.h
@@ -106,13 +106,16 @@ template class HorzMeanOp : public AnalysisOperator {
// Allocate 1D output data array (always Real type)
OutputData = Array1DReal(OutputNames[0], NVertSize);
- // Register output Field with metadata over the vertical dimension
+ // Register output Field with metadata over the vertical dimension. A
+ // horizontal mean has the units and standard name of the field it
+ // averages; its cell_methods record the reduction after any the input
+ // already carries.
I4 NDims = 1;
std::vector DimNames = {VertDimName};
- auto OutputField = Field::create(
+ auto Meta = inheritMetadata(InputNames[0], "area: mean");
+ auto OutputField = createOutputField(
OutputNames[0], "Area-weighted horizontal mean of " + InputNames[0],
- "", // Units
- "", // Standard name
+ Meta, // CF metadata
-std::numeric_limits::max(), // Min valid value
std::numeric_limits::max(), // Max valid value
NDims, // Rank
diff --git a/components/omega/src/analysis/operators/PrefixSumOp.h b/components/omega/src/analysis/operators/PrefixSumOp.h
index 4b45f7dcaedc..f5b5c6bdf284 100644
--- a/components/omega/src/analysis/operators/PrefixSumOp.h
+++ b/components/omega/src/analysis/operators/PrefixSumOp.h
@@ -143,14 +143,34 @@ template class PrefixSumOp : public AnalysisOperator {
OutputNames = {OutputFieldName};
InstanceName = OutputFieldName;
- // Get input metadata
- std::string InputDescr, InputUnits, InputStdName;
+ // Get input metadata. A cumulative sum has the units of the values
+ // summed.
+ std::string InputDescr;
ScalarT InputValidMin, InputValidMax;
InputField->getMetadata("Description", InputDescr);
- InputField->getMetadata("Units", InputUnits);
- InputField->getMetadata("StdName", InputStdName);
InputField->getMetadata("ValidMin", InputValidMin);
InputField->getMetadata("ValidMax", InputValidMax);
+ auto Meta = inheritMetadata(InputNames[0]);
+
+ // The BC field seeds the sum, so it must be in the same units as the
+ // input. If either has unknown units, so does the sum.
+ if (!BCFieldName.empty()) {
+ CFUnits InputUnits, BCUnits;
+ Error Err = CFUnits::parse(Meta.Units, InputUnits);
+ CHECK_ERROR_ABORT(Err, "PrefixSumOp: invalid units of field {}",
+ InputNames[0]);
+ Err = CFUnits::parse(getFieldAttribute(BCFieldName, "units"), BCUnits);
+ CHECK_ERROR_ABORT(Err, "PrefixSumOp: invalid units of BC field {}",
+ BCFieldName);
+ if (InputUnits.isUnknown() || BCUnits.isUnknown()) {
+ Meta.Units = "";
+ } else if (InputUnits != BCUnits) {
+ ABORT_ERROR("PrefixSumOp: BC field {} has units '{}' but field {} "
+ "has units '{}'",
+ BCFieldName, BCUnits.str(), InputNames[0],
+ InputUnits.str());
+ }
+ }
// Allow callers to override the output field name (e.g., for a short
// alias) via the "OutputName" config key.
@@ -158,14 +178,13 @@ template class PrefixSumOp : public AnalysisOperator {
// Create output Field with same dimensions as input
auto OutputField =
- Field::create(OutputNames[0],
- "Cumulative sum of " + InputDescr, // Description
- InputUnits, // Units
- InputStdName, // Standard name
- InputValidMin, // Min valid
- InputValidMax, // Max valid
- NDims, // Rank
- DimNames // Dimension names
+ createOutputField(OutputNames[0],
+ "Cumulative sum of " + InputDescr, // Description
+ Meta, // CF metadata
+ InputValidMin, // Min valid
+ InputValidMax, // Max valid
+ NDims, // Rank
+ DimNames // Dim names
);
// Allocate output data array matching input layout
diff --git a/components/omega/src/analysis/operators/PseudoToGeometricOp.h b/components/omega/src/analysis/operators/PseudoToGeometricOp.h
index 138b38cd66da..7812b01c73d9 100644
--- a/components/omega/src/analysis/operators/PseudoToGeometricOp.h
+++ b/components/omega/src/analysis/operators/PseudoToGeometricOp.h
@@ -97,23 +97,23 @@ template class PseudoToGeometricOp : public AnalysisOperator {
OutputNames = {OutputFieldName};
InstanceName = OutputFieldName;
- // Get input metadata
- std::string PseudoDescr, PseudoUnits, PseudoStdName;
+ // Get input metadata. The conversion factor RhoSw * SpecVol is
+ // dimensionless, so the geometric quantity has the units of the pseudo
+ // quantity.
+ std::string PseudoDescr;
PseudoField->getMetadata("Description", PseudoDescr);
- PseudoField->getMetadata("Units", PseudoUnits);
- PseudoField->getMetadata("StdName", PseudoStdName);
+ auto Meta = inheritMetadata(InputNames[0]);
// Create output Field with updated metadata
- auto OutputField =
- Field::create(OutputNames[0],
- "Geometric conversion of" + PseudoDescr, // Description
- PseudoUnits, // Units
- PseudoStdName, // Standard name
- -std::numeric_limits::max(),
- std::numeric_limits::max(),
- NDims, // Rank
- DimNames // Dimension names
- );
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ "Geometric conversion of" + PseudoDescr, // Description
+ Meta, // CF metadata
+ -std::numeric_limits::max(), // Min valid
+ std::numeric_limits::max(), // Max valid
+ NDims, // Rank
+ DimNames // Dimension names
+ );
// Allocate output data array matching input layout
OutputData = OutputArrayT(OutputNames[0] + "_out", PseudoData.layout());
diff --git a/components/omega/src/analysis/operators/ScalarMultiplyOp.h b/components/omega/src/analysis/operators/ScalarMultiplyOp.h
index c0d2183a59f1..8b13665bd440 100644
--- a/components/omega/src/analysis/operators/ScalarMultiplyOp.h
+++ b/components/omega/src/analysis/operators/ScalarMultiplyOp.h
@@ -32,6 +32,8 @@
#include "AnalysisOperator.h"
+#include
+
namespace OMEGA {
/// ScalarMultiplyOp multiplies all elements of an input field by a scalar
@@ -65,10 +67,14 @@ template class ScalarMultiplyOp : public AnalysisOperator {
// Read required scalar parameter from configuration.
// Read as string first to preserve the original representation for field
// naming (std::to_string loses precision, e.g. "1.0e-6" -> "0.000001").
+ // The units are scaled with the scalar in double precision, so that in
+ // a single-precision build 1e-6 still gives units of 1e+06 m3 s-1.
std::string ScalarStr;
+ double ScalarR8;
Error Err = Options.get("Scalar", ScalarStr);
if (Err.isSuccess()) {
- Scalar = static_cast(std::stod(ScalarStr));
+ ScalarR8 = std::stod(ScalarStr);
+ Scalar = static_cast(ScalarR8);
} else {
// Fallback: try reading directly as Real
Err = Options.get("Scalar", Scalar);
@@ -78,6 +84,7 @@ template class ScalarMultiplyOp : public AnalysisOperator {
"in configuration");
}
ScalarStr = std::to_string(Scalar);
+ ScalarR8 = static_cast(Scalar);
}
// Read optional InPlace parameter (default: false)
@@ -104,24 +111,33 @@ template class ScalarMultiplyOp : public AnalysisOperator {
InstanceName = OutputFieldName;
// Get input metadata
- std::string InputDescr, InputUnits, InputStdName;
- ScalarT InputValidMin, InputValidMax, InputFillValue;
+ std::string InputDescr;
+ ScalarT InputValidMin, InputValidMax;
InputField->getMetadata("Description", InputDescr);
- InputField->getMetadata("StdName", InputStdName);
InputField->getMetadata("ValidMin", InputValidMin);
InputField->getMetadata("ValidMax", InputValidMax);
+ // Multiplying the values by the scalar scales the units by its
+ // reciprocal, so a transport in m3 s-1 multiplied by 1e-6 is in
+ // 1e+06 m3 s-1 (Sverdrups). A negative scalar also reverses the sign of
+ // the quantity, which then no longer matches the input standard name. A
+ // zero scalar gives zeros, which are valid in any units.
+ auto Meta = inheritMetadata(InputNames[0]);
+ if (ScalarR8 != 0)
+ Meta.Units = CFUnits::scale(Meta.Units, 1.0 / std::abs(ScalarR8));
+ if (ScalarR8 < 0)
+ Meta.StdName = "";
+
// Create output Field with same dimensions as input
auto OutputField =
- Field::create(OutputNames[0],
- InputDescr + " multiplied by " +
- std::to_string(Scalar), // Description
- "", // Units
- InputStdName, // Standard name
- InputValidMin, // Min valid
- InputValidMax, // Max valid
- NDims, // Rank
- DimNames // Dimension names
+ createOutputField(OutputNames[0],
+ InputDescr + " multiplied by " +
+ std::to_string(Scalar), // Description
+ Meta, // CF metadata
+ InputValidMin, // Min valid
+ InputValidMax, // Max valid
+ NDims, // Rank
+ DimNames // Dimension names
);
// Store array size for parallel iteration
diff --git a/components/omega/src/analysis/operators/SpatialMaxOp.h b/components/omega/src/analysis/operators/SpatialMaxOp.h
index ab22b1ba5140..b40bd9bea66b 100644
--- a/components/omega/src/analysis/operators/SpatialMaxOp.h
+++ b/components/omega/src/analysis/operators/SpatialMaxOp.h
@@ -65,17 +65,20 @@ template class SpatialMaxOp : public AnalysisOperator {
DimNames[0] = "Scalar";
auto ScalarDim = Dimension::create(DimNames[0], 1);
- // Register output Field with metadata
- auto OutputField =
- Field::create(OutputNames[0],
- "Spatial maximum of " + InputNames[0], // Description
- "", // Units
- "", // Standard name
- -std::numeric_limits::max(), // Min valid value
- std::numeric_limits::max(), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
- );
+ // Register output Field with metadata. A maximum has the units and
+ // standard name of the field it reduces; its cell_methods record the
+ // reduction after any the input already carries.
+ auto CellMethod = spatialCellMethod(InputNames[0], "maximum");
+ auto Meta = inheritMetadata(InputNames[0], CellMethod);
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ "Spatial maximum of " + InputNames[0], // Description
+ Meta, // CF metadata
+ -std::numeric_limits::max(), // Min valid value
+ std::numeric_limits::max(), // Max valid value
+ NDims, // Rank
+ DimNames // Dimension names
+ );
// Attach output data array to Field
OutputField->template attachData>(OutputData);
diff --git a/components/omega/src/analysis/operators/SpatialMeanOp.h b/components/omega/src/analysis/operators/SpatialMeanOp.h
index ebff0862b86a..fc6b39fe5bc1 100644
--- a/components/omega/src/analysis/operators/SpatialMeanOp.h
+++ b/components/omega/src/analysis/operators/SpatialMeanOp.h
@@ -66,16 +66,19 @@ template class SpatialMeanOp : public AnalysisOperator {
DimNames[0] = "Scalar";
auto ScalarDim = Dimension::create(DimNames[0], 1);
- // Register output Field with metadata
+ // Register output Field with metadata. A mean has the units and
+ // standard name of the field it reduces; its cell_methods record the
+ // reduction after any the input already carries.
+ auto Meta = inheritMetadata(InputNames[0],
+ spatialCellMethod(InputNames[0], "mean"));
auto OutputField =
- Field::create(OutputNames[0],
- "Spatial mean of " + InputNames[0], // Description
- "", // Units
- "", // Standard name
- -std::numeric_limits::max(), // Min valid value
- std::numeric_limits::max(), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
+ createOutputField(OutputNames[0],
+ "Spatial mean of " + InputNames[0], // Description
+ Meta, // CF metadata
+ -std::numeric_limits::max(), // Min valid
+ std::numeric_limits::max(), // Max valid
+ NDims, // Rank
+ DimNames // Dim names
);
// Attach output data array to Field
diff --git a/components/omega/src/analysis/operators/SpatialMinOp.h b/components/omega/src/analysis/operators/SpatialMinOp.h
index dcd2c98f3d59..c0b57051119e 100644
--- a/components/omega/src/analysis/operators/SpatialMinOp.h
+++ b/components/omega/src/analysis/operators/SpatialMinOp.h
@@ -65,17 +65,20 @@ template class SpatialMinOp : public AnalysisOperator {
DimNames[0] = "Scalar";
auto ScalarDim = Dimension::create(DimNames[0], 1);
- // Register output Field with metadata
- auto OutputField =
- Field::create(OutputNames[0],
- "Spatial minimum of " + InputNames[0], // Description
- "", // Units
- "", // Standard name
- -std::numeric_limits::max(), // Min valid value
- std::numeric_limits::max(), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
- );
+ // Register output Field with metadata. A minimum has the units and
+ // standard name of the field it reduces; its cell_methods record the
+ // reduction after any the input already carries.
+ auto CellMethod = spatialCellMethod(InputNames[0], "minimum");
+ auto Meta = inheritMetadata(InputNames[0], CellMethod);
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ "Spatial minimum of " + InputNames[0], // Description
+ Meta, // CF metadata
+ -std::numeric_limits::max(), // Min valid value
+ std::numeric_limits::max(), // Max valid value
+ NDims, // Rank
+ DimNames // Dimension names
+ );
// Attach output data array to Field
OutputField->template attachData>(OutputData);
diff --git a/components/omega/src/analysis/operators/SpatialStdDevOp.h b/components/omega/src/analysis/operators/SpatialStdDevOp.h
index dff76cbb712f..492048f5afe4 100644
--- a/components/omega/src/analysis/operators/SpatialStdDevOp.h
+++ b/components/omega/src/analysis/operators/SpatialStdDevOp.h
@@ -67,17 +67,21 @@ template class SpatialStdDevOp : public AnalysisOperator {
DimNames[0] = "Scalar";
auto ScalarDim = Dimension::create(DimNames[0], 1);
- // Register output Field with metadata
- auto OutputField =
- Field::create(OutputNames[0],
- "Standard deviation of " + InputNames[0], // Description
- "", // Units
- "", // Standard name
- static_cast(0), // Min valid value
- std::numeric_limits::max(), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
- );
+ // Register output Field with metadata. A standard deviation has the
+ // units and standard name of the field it reduces; its cell_methods
+ // record the reduction after any the input already carries.
+ auto Meta = inheritMetadata(
+ InputNames[0],
+ spatialCellMethod(InputNames[0], "standard_deviation"));
+ auto OutputField = createOutputField(
+ OutputNames[0],
+ "Standard deviation of " + InputNames[0], // Description
+ Meta, // CF metadata
+ static_cast(0), // Min valid value
+ std::numeric_limits::max(), // Max valid value
+ NDims, // Rank
+ DimNames // Dimension names
+ );
// Attach output data array to Field
OutputField->template attachData(OutputData);
diff --git a/components/omega/src/analysis/operators/TimeMeanOp.h b/components/omega/src/analysis/operators/TimeMeanOp.h
index bddb93a5534d..075709c57153 100644
--- a/components/omega/src/analysis/operators/TimeMeanOp.h
+++ b/components/omega/src/analysis/operators/TimeMeanOp.h
@@ -83,30 +83,28 @@ template class TimeMeanOp : public AnalysisOperator {
std::vector DimNames;
InputField->getDimNames(DimNames);
- // Fetch input metadata
- std::string InputDescr, InputUnits, InputStdName;
+ // Fetch the input description and valid range
+ std::string InputDescr;
ScalarT InputValidMin, InputValidMax;
InputField->getMetadata("Description", InputDescr);
- InputField->getMetadata("Units", InputUnits);
- InputField->getMetadata("StdName", InputStdName);
InputField->getMetadata("ValidMin", InputValidMin);
InputField->getMetadata("ValidMax", InputValidMax);
- // Register output Field with same dimensions as input but Real type
+ // Register output Field with same dimensions as input but Real type.
+ // A time mean has the units and standard name of the field it
+ // averages; its cell_methods record the reduction after any the input
+ // already carries (e.g. "area: mean time: mean").
+ auto Meta = inheritMetadata(InputNames[0], "time: mean");
auto OutputField =
- Field::create(OutputNames[0],
- "Time average of " + InputDescr, // Description
- InputUnits, // Units
- InputStdName, // Standard name
- static_cast(InputValidMin), // Min valid value
- static_cast(InputValidMax), // Max valid value
- NDims, // Rank
- DimNames // Dimension names
+ createOutputField(OutputNames[0],
+ "Time average of " + InputDescr, // Description
+ Meta, // CF metadata
+ static_cast(InputValidMin), // Min valid
+ static_cast(InputValidMax), // Max valid
+ NDims, // Rank
+ DimNames // Dim names
);
- // Stamp CF-compliant cell_methods for time-mean output.
- OutputField->addMetadata("cell_methods", std::string("time: mean"));
-
// Store array size for parallel iteration
ArraySize = static_cast(InputData.size());
diff --git a/components/omega/src/analysis/operators/TransectAccumulatorOp.h b/components/omega/src/analysis/operators/TransectAccumulatorOp.h
index 79cc4fdcd79b..8ec288f0a0ab 100644
--- a/components/omega/src/analysis/operators/TransectAccumulatorOp.h
+++ b/components/omega/src/analysis/operators/TransectAccumulatorOp.h
@@ -144,28 +144,27 @@ class TransectAccumulatorOp : public AnalysisOperator {
OutputNames = {OutputFieldName};
InstanceName = OutputFieldName;
- // Get input metadata
- std::string TransportDescr, TransportUnits, TransportStdName;
+ // Get input metadata. A signed sum of the transport through the
+ // transect edges has the units of the transport.
+ std::string TransportDescr;
ScalarT TransportValidMin, TransportValidMax;
TransportField->getMetadata("Description", TransportDescr);
- TransportField->getMetadata("Units", TransportUnits);
- TransportField->getMetadata("StdName", TransportStdName);
TransportField->getMetadata("ValidMin", TransportValidMin);
TransportField->getMetadata("ValidMax", TransportValidMax);
+ auto Meta = inheritMetadata(InputNames[0]);
// Create output Field dimensions (1D: NVertLayers only)
std::vector OutputDimNames = {TransportDimNames[1]};
// Create output Field
- auto OutputField = Field::create(
+ auto OutputField = createOutputField(
OutputNames[0],
"Transect " + TransectName + " accumulated transport", // Description
- TransportUnits, // Units
- TransportStdName, // Standard name
- TransportValidMin, // Min valid
- TransportValidMax, // Max valid
- 1, // Rank (1D)
- OutputDimNames // Dimension names
+ Meta, // CF metadata
+ TransportValidMin, // Min valid
+ TransportValidMax, // Max valid
+ 1, // Rank (1D)
+ OutputDimNames // Dim names
);
// Allocate output and local accumulation arrays (both 1D)
diff --git a/components/omega/src/infra/CFUnits.cpp b/components/omega/src/infra/CFUnits.cpp
new file mode 100644
index 000000000000..ad8abd692ab8
--- /dev/null
+++ b/components/omega/src/infra/CFUnits.cpp
@@ -0,0 +1,367 @@
+//===-- infra/CFUnits.cpp - units algebra for CF units strings --*- C++ -*-===//
+//
+// Implementation of the CFUnits class. A units string is split on slashes
+// into a numerator and denominators, each of those on whitespace into
+// factors, and each factor into a unit symbol and an optional integer
+// exponent. Factors are kept in order of first appearance so that the
+// formatted result reads like the conventional spelling (kg m-3, not
+// m-3 kg). A leading number is the scale factor of a scaled unit.
+//
+//===----------------------------------------------------------------------===//
+
+#include "CFUnits.h"
+#include "Logging.h"
+
+#include
+#include
+#include
+#include
+#include
+#include