diff --git a/components/omega/configs/Default.yml b/components/omega/configs/Default.yml index a4dcc2165835..b7c75527f789 100644 --- a/components/omega/configs/Default.yml +++ b/components/omega/configs/Default.yml @@ -202,11 +202,37 @@ Omega: Analysis: GlobalStats: Enable: false - Fields: ["NormalVelocity", "PseudoThickness", "Temperature", "Salinity"] - SpatialStats: ["Max", "Min", "Mean", "StdDev"] - ReductionPeriod: ["1Day", "1Month"] - SnapshotPeriod: ["6Hours"] + # Global Statistics (GlobalStats) analysis group + # 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] + # List of field names to compute statistics for + SpatialStats: [Max, Min, Mean, StdDev] + # Spatial statistics to compute (one per field) + ReductionPeriod: [1Day, 1Month] + # Temporal reduction periods (time-averaged stats) + SnapshotPeriod: [6Hours] + # Instantaneous snapshot output intervals Filename: global.stats.$Y Stream: FileFreq: 1 FileFreqUnits: years + MOC: + Enable: false + # 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: [1Month] # Temporal reduction periods + SnapshotPeriod: [] # Instantaneous output periods + Filename: moc.$Y + Stream: + FileFreq: 1 + FileFreqUnits: months diff --git a/components/omega/doc/devGuide/Analysis.md b/components/omega/doc/devGuide/Analysis.md index e8ecb7ebf016..1b1230e3c33c 100644 --- a/components/omega/doc/devGuide/Analysis.md +++ b/components/omega/doc/devGuide/Analysis.md @@ -200,6 +200,14 @@ field(s). | `SpatialMean` | 1 | 1 | scalar (`Array1DReal`, dimension `Scalar`) | `_SpatialMean` | — | Global mean of the input field. | | `SpatialStdDev` | 2 (the field and its `_SpatialMean`) | 1 | scalar (`Array1DReal`, dimension `Scalar`) | `_SpatialStdDev` | — | Global standard deviation of the input field. Requires the field's `SpatialMean` as an upstream input, which is added to its input list automatically. | | `TimeMean` | 1 | 1 | same rank and dimensions as the input (`Real`) | `_TimeMean` | `Period` (string, e.g. `"1Day"`) | Time average of the input field over a configurable period (e.g. `1Day`). Accumulates every time step and finalizes the mean when the period alarm rings. Output name embeds the period, e.g. `_TimeMean1Day`. | +| `BinaryMultiply` | 2 | 1 | same as first input | `_BinaryMultiply()` | — | Element-wise multiplication of two fields. Supports vertical expansion (1D field replicated across vertical layers when multiplied with 2D/3D field). | +| `BinnedAccumulator` | 2 (data field, bin index field) | 1 | replaces horizontal dimension with `NumBins` | `_BinnedAccumulator()` | `NumBins` (I4) | Accumulates field values into spatial bins. Uses MPI reduction for global totals. Automatically applies regional mask if attached to input Field. | +| `CoordinateBinning` | 1 | 1 | 1D integer (`Array1DI4`) | `_BinIndex` | `NumBins` (I4), `MinBin` (Real, optional), `MaxBin` (Real, optional) | Assigns mesh entities to bins based on coordinate values (e.g., latitude). Computed once during initialization and cached. | +| `ExtractRegion` | 1 | 1 | same as input | `_` | `MaskName` (string) | Applies regional mask to field by multiplying values with mask. Attaches mask to output Field for downstream operators. Supports mask intersection. | +| `PrefixSum` | 1 | 1 | same as input | `_PrefixSum` | `Dimension` (I4), `Reverse` (bool, default false) | Cumulative summation along specified dimension. Forward (start→end) or reverse (end→start) scan. | +| `PseudoToGeometric` | 1 | 1 | same as input | `_Geometric` | — | Converts pseudo-height coordinates to geometric coordinates using specific volume. Handles vertical grid staggering automatically. | +| `ScalarMultiply` | 1 | 1 | same as input | `_ScalarMultiply()` | `Scalar` (string, parsed to Real) | Multiplies field by scalar constant. Can be used for unit conversions (e.g., ×1e-6 for Sverdrups). | +| `TransectAccumulator` | 3 (data field, mask field, sign field) | 1 | 1D vertical profile | `_TransectAccumulator()` | `TransectName` (string) | Accumulates transport across transect edges. Only supports 2D Real arrays for the data field. Uses MPI reduction for global totals. | ## Operator factory and type dispatch @@ -220,6 +228,19 @@ are 1 through 3 (the 4D and 5D entries are present but commented out because Omega currently utilizes no arrays with rank $>$ 3, and each registered operator variant increases compile time and binary size). +For operators that only support specific array ranks, the factory provides +specialized registration methods using SFINAE (Substitution Failure Is Not An +Error) to prevent invalid template instantiations at compile time: + +- `register1DVariants` — registers only 1D array variants (horizontal fields without vertical structure) +- `register2DVariants` — registers only 2D array variants (horizontal × vertical) +- `register2DRealVariants` — registers only 2D Real array variants (excludes integer types) + +These methods use compile-time type checking to filter out incompatible array +types, reducing compile time and binary size while catching type errors early. +The SFINAE helpers (`register1DVariantHelper`, etc.) expand over all array types +but only register the matching variants, with no-op branches for mismatched types. + At creation time, the orchestrator calls: ```c++ auto Op = AnalysisOpFactory::createOp(OpType, UpstreamNames, Options); @@ -250,6 +271,22 @@ spatial reduction; a `Time` prefix denotes a temporal reduction, with the period parsed from the first digit in the token) and the operator is created via the factory and wrapped in an `OperatorNode`. +**Parenthesized arguments:** The parser supports operators with parenthesized +arguments for passing field names, scalar values, or other parameters. The +parser respects parenthesis depth when splitting on underscores, so +`Field_BinaryMultiply(OtherField)_ScalarMultiply(1.0e-6)` correctly parses as +three tokens: `Field`, `BinaryMultiply(OtherField)`, and `ScalarMultiply(1.0e-6)`. +Different operators interpret parenthesized arguments differently: +- `BinaryMultiply(FieldName)` — second input field name +- `ScalarMultiply(Value)` — scalar value (parsed from string to Real) +- `ExtractRegion(MaskName)` — mask field name passed via Config +- `BinnedAccumulator(BinIndexField)` — bin index field name +- `TransectAccumulator(TransectName)` — transect name for mask lookup + +An optional `Config` parameter can be passed to `parseChainAndBuildOps()` to +provide operator-specific configuration that applies to all operators in the +chain (e.g., binning parameters, integration direction). + After all groups have registered their chains, the constructor resolves the graph edges with `buildOperatorDependencies()`, which matches each node's input Field names against the output Field names of every other node and @@ -350,6 +387,61 @@ the discrete-sampling chain for instantaneous output. It then calls `createAnalysisGroupStreams()` to create the corresponding output streams. At least one of `ReductionPeriod` or `SnapshotPeriod` must be present. +### MOC + +`MOC` computes the Meridional Overturning Circulation (MOC) streamfunction +using two methods: latitude-binned regional MOC and transect-based MOC. +Configuration specifies binning parameters, region/transect names, and output +frequencies: + +```yaml +MOC: + Enable: true + NumBins: 180 # Number of latitude bins + MinLat: -90.0 # Minimum latitude in degrees + MaxLat: 90.0 # Maximum latitude in degrees + Regions: [Global, Atlantic] # Regional MOC (latitude × depth) + Transects: [Drake, Atlantic26N] # Transect MOC (depth only) + ReductionPeriod: [1Month] + SnapshotPeriod: [1Day] +``` + +**Latitude-binned MOC** computes the streamfunction as a function of latitude +and depth for each specified region. The constructor builds a complex operator +chain for each region: +1. `LatCell_CoordinateBinning` — assigns cells to latitude bins (initialization only, shared across regions) +2. `VerticalPseudoVelocity_PseudoToGeometric` — converts to geometric coordinates +3. `VerticalVelocity_BinaryMultiply(AreaCell)` — computes vertical flux +4. `[Optional] VerticalFlux_ExtractRegion(RegionMask)` — applies regional mask (for non-global regions) +5. `VerticalFlux_BinnedAccumulator(LatCell_BinIndex)` — accumulates flux into latitude bins +6. `BinnedFlux_PrefixSum` — horizontal integration (south to north) +7. `MOC_ScalarMultiply(1.0e-6)` — converts to Sverdrups (Sv) + +**Transect-based MOC** computes the streamfunction across specific transects +as a function of depth only. The constructor builds a chain for each transect: +1. `PseudoThickness_PseudoToGeometric` — converts to geometric layer thickness +2. `LayerThickness_BinaryMultiply(NormalVelocity)` — thickness × velocity +3. `EdgeTransport_BinaryMultiply(DvEdge)` — transport × edge width +4. `TransportField_TransectAccumulator(TransectName)` — accumulates across transect edges +5. `TransectTransport_PrefixSum` — vertical integration (bottom to top, reverse) +6. `TransectMOC_ScalarMultiply(1.0e-6)` — converts to Sverdrups + +For each chain, the constructor stores an operator configuration (`Config`) +containing parameters like `NumBins`, `Dimension`, `Reverse`, and `TransectName` +in a `ChainConfigs` vector. These configs are passed to `buildTemporalChains()`, +which appends temporal operators and applies custom `IOName` metadata to +produce user-friendly output variable names (e.g., `MOC_streamfunction_Global` +instead of the full operator chain string). At least one of `Regions` or +`Transects` must be specified. + +```{note} +Regional and transect masks are not yet implemented in the Omega infrastructure. +The `Regions` configuration currently only supports `[Global]`, and `Transects` +is a placeholder. The operator infrastructure (ExtractRegionOp, +TransectAccumulatorOp) is fully implemented and ready for use once mask fields +become available. +``` + ## Extensibility The module is designed so that new operators and new groups plug in through diff --git a/components/omega/doc/devGuide/Field.md b/components/omega/doc/devGuide/Field.md index b295ff9609d7..993a1fc6d89c 100644 --- a/components/omega/doc/devGuide/Field.md +++ b/components/omega/doc/devGuide/Field.md @@ -263,6 +263,37 @@ and before exiting, all fields should be removed using: Field::clear(); ``` +Fields can optionally have a regional mask attached to support spatial subsetting +in analysis operations. A regional mask is a 1D integer array (`Array1DI4`) over +the horizontal dimension (cells, edges, or vertices) where mask values indicate +inclusion (1) or exclusion (0) of each location. + +Regional masks are set, queried, and retrieved using: +```c++ + // Set a regional mask for a field (shallow copy - shares data with source) + MyField->setRegionalMask(MaskArray); + + // Check if a field has a regional mask attached + bool HasMask = MyField->hasRegionalMask(); + + // Retrieve the regional mask (returns Array1DI4) + Array1DI4 Mask = MyField->getRegionalMask(); +``` + +Key implementation details: +- The mask is stored as a shallow copy (Kokkos view), so the original mask array and the field's mask share the same data +- `hasRegionalMask()` returns true if `setRegionalMask()` was called, even if the pointer is null (used for lifetime bug detection) +- `getRegionalMask()` validates pointer integrity and aborts if mask was set but deallocated +- Regional masks are automatically propagated through analysis operator chains: + - Binary operators (e.g., BinaryMultiplyOp) propagate mask from first input + - ExtractRegionOp attaches masks and can compute mask intersections + - Spatial reduction operators (e.g., BinnedAccumulatorOp) apply masks automatically + +Regional masks are primarily used by the Analysis framework for regional statistics +and diagnostics (e.g., Atlantic MOC, basin-averaged temperatures). The mask mechanism +enables efficient spatial subsetting without modifying field data or creating separate +regional field copies. + As mentioned above, Fields can be assigned to groups to provide an easy way to reference fields that commonly appear together, especially when listing contents of fields in IO files. Internally, a field group is implemented as diff --git a/components/omega/doc/devGuide/IOStreams.md b/components/omega/doc/devGuide/IOStreams.md index f1ea8af3e23b..78d13873930c 100644 --- a/components/omega/doc/devGuide/IOStreams.md +++ b/components/omega/doc/devGuide/IOStreams.md @@ -100,6 +100,28 @@ The Metadata corresponding to ForcingTime will then be read from the file and inserted as the Metadata value. If no metadata is to be read from the file, then an empty ReqMetadata variable can be passed. +### IOName Metadata for User-Friendly Output Variable Names + +Fields written to netCDF files can optionally specify a custom output variable +name using the `IOName` metadata field. By default, IOStream uses the field's +internal name (which may be a long operator chain string like +`VerticalPseudoVelocity_PseudoToGeometric_BinaryMultiply_...`) as the netCDF +variable name. Setting `IOName` metadata allows specifying a more readable name +for end users: + +```c++ + MyField->addMetadata("IOName", "MOC_streamfunction_Global"); +``` + +During output, IOStream checks for the presence of `IOName` metadata and uses it +as the netCDF variable name if found, falling back to the field's internal name +otherwise. The `IOName` metadata is automatically filtered out of CF attribute +writing since it is an internal control key, not a CF-compliant attribute. + +This feature is particularly useful for analysis outputs where operator chain +names are long and technical, but output files should have concise, descriptive +variable names. + As described in the [User Guide](#omega-user-iostreams), all streams are defined in the input configuration file and most other IOStream functions are associated either with that initialization or to support the read/write diff --git a/components/omega/doc/userGuide/Analysis.md b/components/omega/doc/userGuide/Analysis.md index 29c751c38051..3b78ef3027c0 100644 --- a/components/omega/doc/userGuide/Analysis.md +++ b/components/omega/doc/userGuide/Analysis.md @@ -147,6 +147,80 @@ Automatically created and named: - Instantaneous output: `GlobalStats_FreqInstants` (e.g., `GlobalStats_6HourInstants`) +### MOC + +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. + +**Example:** + +```yaml +Omega: + Analysis: + MOC: + Enable: true + NumBins: 180 + MinLat: -90.0 + MaxLat: 90.0 + Regions: [Global] + Transects: [] + ReductionPeriod: [1Month] + SnapshotPeriod: [1Day] + Filename: moc.$Y-$M + Stream: + FileFreq: 1 + FileFreqUnits: months +``` + +**Group-Specific Parameters:** + +- **NumBins:** Optional integer specifying the number of latitude bins for + regional MOC (default: 180, approximately 1-degree resolution). + +- **MinLat:** Optional minimum latitude in degrees for binning (default: -90.0). + +- **MaxLat:** Optional maximum latitude in degrees for binning (default: 90.0). + +- **Regions:** Optional list of region names for regional MOC computation + (default: `[Global]`). Regional MOC computes the streamfunction as a function + of latitude and depth. Currently only `Global` is supported; region masks are + not yet implemented. + +- **Transects:** Optional list of transect names for transect-based MOC + computation (default: `[]`). Transect MOC computes the streamfunction as a + function of depth only by accumulating transport across specified transect + edges. Transect masks are not yet implemented. + +- **ReductionPeriod:** Optional list of time periods for temporal reduction. + Each period must divide evenly into the restart interval. At least one of + `ReductionPeriod` or `SnapshotPeriod` must be specified. + +- **SnapshotPeriod:** Optional list of intervals for instantaneous output. At + least one of `ReductionPeriod` or `SnapshotPeriod` must be specified. + +**Output fields:** + +- Regional MOC: `MOC_streamfunction_RegionName` (2D: latitude bins × depth) +- Transect MOC: `MOC_streamfunction_transect_TransectName` (1D: depth only) + +For temporal reduction, `_TimeMeanPeriod` is appended (e.g., +`MOC_streamfunction_Global_TimeMean1Month`). + +**Output streams:** +Automatically created and named: + - Time reduction: `MOC_FreqTimeStats` (e.g., `MOC_1MonthTimeStats`) + - Instantaneous output: `MOC_FreqInstants` (e.g., `MOC_1DayInstants`) + +```{note} +Regional and transect masks are not yet implemented. The `Regions` parameter +currently only supports `[Global]`, and `Transects` is a placeholder for future +functionality. The underlying operator infrastructure is complete and ready for +use once the mask fields are implemented in Omega. +``` + ## Usage Notes ### Temporal Reduction Period Constraint diff --git a/components/omega/src/analysis/Analysis.cpp b/components/omega/src/analysis/Analysis.cpp index 3307079d530d..12f5dbd0a855 100644 --- a/components/omega/src/analysis/Analysis.cpp +++ b/components/omega/src/analysis/Analysis.cpp @@ -152,6 +152,9 @@ Analysis::Analysis(const std::string &InName, const MachEnv *InEnv, GlobalStats GlobalStatsGroup(NamePrefix + GroupName, GroupCfg, this); continue; + } else if (GroupName == "MOC") { + MOC MOCGroup(NamePrefix + GroupName, GroupCfg, this); + continue; } // User-defined custom groups not yet supported @@ -174,17 +177,34 @@ Analysis::Analysis(const std::string &InName, const MachEnv *InEnv, //------------------------------------------------------------------------------ // Parses an underscore-delimited operator chain string and instantiates // operators for each node in the chain. For example, -// "Temperature_SpatialMean_TimeMean1day" parses into three operators. +// "Temperature_SpatialMean_TimeMean1day" parses into two operators +// (SpatialMeanOp and TimeMeanOp; Temperature is the source field). // If an intermediate operator already exists (shared by another chain), // it is reused rather than duplicated. This natural sharing mechanism // avoids redundant computation of common intermediate results. -void Analysis::parseChainAndBuildOps(const std::string &OpChainStr) { +void Analysis::parseChainAndBuildOps(const std::string &OpChainStr, + const Config &OpConfig) { - // Split the chain string on underscore delimiters + // Split the chain string on underscore delimiters, but not within + // parentheses std::vector ChainVec; - std::stringstream OpChainSS(OpChainStr); std::string Part; - while (std::getline(OpChainSS, Part, '_')) { + int Depth = 0; + for (char C : OpChainStr) { + if (C == '(') { + ++Depth; + Part += C; + } else if (C == ')') { + --Depth; + Part += C; + } else if (C == '_' && Depth == 0) { + ChainVec.push_back(Part); + Part.clear(); + } else { + Part += C; + } + } + if (!Part.empty()) { ChainVec.push_back(Part); } @@ -206,7 +226,7 @@ void Analysis::parseChainAndBuildOps(const std::string &OpChainStr) { // Spatial operators (SpatialMean, SpatialMax, etc.) if (ChainNode.find("Spatial") != std::string::npos) { - registerAnalysisOp(ChainNode, {Upstream}, makeOpConfig()); + registerAnalysisOp(ChainNode, {Upstream}, OpConfig); continue; } @@ -220,10 +240,104 @@ void Analysis::parseChainAndBuildOps(const std::string &OpChainStr) { } std::string TimeOp = ChainNode.substr(0, Pos); std::string FreqStr = ChainNode.substr(Pos); - registerAnalysisOp(TimeOp, {Upstream}, - makeOpConfig(opParam("Period", FreqStr))); + + // Merge OpConfig with Period parameter for temporal operators + Config TimeOpConfig = OpConfig; + TimeOpConfig.add("Period", FreqStr); + registerAnalysisOp(TimeOp, {Upstream}, TimeOpConfig); continue; } + + // PseudoToGeometric operator + // Note: This operator creates output field named Upstream + + // "_Geometric" so we need to update CurChainStr to match the actual + // field name + if (ChainNode == "PseudoToGeometric") { + registerAnalysisOp(ChainNode, {Upstream}, OpConfig); + continue; + } + + // CoordinateBinning operator + if (ChainNode == "CoordinateBinning") { + registerAnalysisOp(ChainNode, {Upstream}, OpConfig); + continue; + } + + // PrefixSum operator - plain or with optional BC argument + // Syntax: "PrefixSum" or "PrefixSum(BC=SomeFieldName)" + if (ChainNode == "PrefixSum") { + registerAnalysisOp(ChainNode, {Upstream}, OpConfig); + continue; + } + if (ChainNode.substr(0, 10) == "PrefixSum(") { + auto LParen = ChainNode.find('('); + auto RParen = ChainNode.find(')'); + if (RParen == std::string::npos || RParen < LParen) { + ABORT_ERROR( + "Analysis: Mismatched parentheses in PrefixSum token {}", + ChainNode); + } + std::string ArgStr = + ChainNode.substr(LParen + 1, RParen - LParen - 1); + // Expected format: "BC=FieldName" + if (ArgStr.substr(0, 3) == "BC=") { + std::string BCFieldName = ArgStr.substr(3); + registerAnalysisOp("PrefixSum", {Upstream, BCFieldName}, + OpConfig); + } else { + ABORT_ERROR("Analysis: Unknown argument '{}' in PrefixSum token " + "{}. Expected 'BC=FieldName'.", + ArgStr, ChainNode); + } + continue; + } + + // Parenthesized-argument operators: e.g. "BinaryMultiply(Field2)" + // or "ScalarMultiply(1.0e-6)" or "ExtractRegion(Atlantic)" + // For BinaryMultiply, the argument is a second upstream field name. + // For ScalarMultiply, the argument is a scalar value passed via + // Config. For ExtractRegion, the argument is a mask name passed via + // Config. + auto LParen = ChainNode.find('('); + if (LParen != std::string::npos) { + auto RParen = ChainNode.find(')'); + if (RParen == std::string::npos || RParen < LParen) { + ABORT_ERROR("Analysis: Mismatched parentheses in chain token {}", + ChainNode); + } + std::string OpName = ChainNode.substr(0, LParen); + std::string ArgStr = + ChainNode.substr(LParen + 1, RParen - LParen - 1); + + // ScalarMultiply takes a scalar value, not a field name + if (OpName == "ScalarMultiply") { + Config ScalarOpConfig = OpConfig; + ScalarOpConfig.add("Scalar", ArgStr); + registerAnalysisOp(OpName, {Upstream}, ScalarOpConfig); + } else if (OpName == "ExtractRegion") { + // ExtractRegion takes a mask name, not a field name + Config ExtractRegionConfig = OpConfig; + ExtractRegionConfig.add("MaskName", ArgStr); + registerAnalysisOp(OpName, {Upstream}, ExtractRegionConfig); + } else if (OpName == "BinnedAccumulator") { + // BinnedAccumulator takes data field (Upstream) and bin index + // field (ArgStr) + registerAnalysisOp(OpName, {Upstream, ArgStr}, OpConfig); + } else if (OpName == "TransectAccumulator") { + // TransectAccumulator takes data field (Upstream) and transect + // name (ArgStr) Construct mask and sign field names from + // transect name + std::string MaskFieldName = "TransectEdgeMask" + ArgStr; + std::string SignFieldName = "TransectEdgeMaskSign" + ArgStr; + registerAnalysisOp( + OpName, {Upstream, MaskFieldName, SignFieldName}, OpConfig); + } else { + // Other operators (like BinaryMultiply) take a field name + registerAnalysisOp(OpName, {Upstream, ArgStr}, OpConfig); + } + continue; + } + ABORT_ERROR("Analysis: Error trying to parse {}. No Field or " "Operator named {}", OpChainStr, ChainNode); diff --git a/components/omega/src/analysis/Analysis.h b/components/omega/src/analysis/Analysis.h index b6479173da70..c709c1e5bcd7 100644 --- a/components/omega/src/analysis/Analysis.h +++ b/components/omega/src/analysis/Analysis.h @@ -119,9 +119,13 @@ class Analysis { /// all operators in the chain that do not yet exist as Fields. For /// example, "Temperature_SpatialMean_TimeMean1day" parses into three /// operators. If intermediate operators already exist (shared by other - /// chains), they are reused rather than duplicated. + /// chains), they are reused rather than duplicated. Optionally accepts + /// a Config object containing parameters for operators in the chain. void parseChainAndBuildOps( - const std::string &OpChainStr ///< [in] underscore-delimited chain string + const std::string + &OpChainStr, ///< [in] underscore-delimited chain string + const Config &OpConfig = + Config() ///< [in] optional operator configuration ); /// Instantiates a single operator via the factory and appends it as @@ -151,6 +155,26 @@ class Analysis { bool OpNodeExists(const std::string &FullOpName ///< [in] full op name ); + /// Post-hoc dependency resolution: iterates over all operator nodes and + /// matches input field names against other nodes' output field names to + /// populate the Upstreams vectors. This forms the edges of the + /// dependency graph. In future versions, this will be replaced by + /// signature-based deduplication during graph construction. + void buildOperatorDependencies(); + + /// Sets ComputeAlarms on terminal nodes by borrowing alarm pointers from + /// associated IOStream instances. For temporal reduction operators, also + /// creates accumulation alarms and adds them to ComputeAlarms. Then + /// calls propagateAlarmsUpstream() to propagate alarms to upstream + /// dependencies. + void setComputeAlarms(); + + /// Calls initialize() on all operators after the dependency graph is + /// complete and all Fields exist. This allows operators to store + /// pointers to mesh, environment, and other resources needed during + /// compute(). + void initializeAllOps(); + /// Retrieves the default Analysis instance. The preference is to pass /// the Analysis pointer as an argument, but retrieval is necessary for /// sharing info between initialization and run phases. @@ -203,26 +227,6 @@ class Analysis { /// variants (scalar types, ranks, memory locations). Defined in Ops.cpp. static void registerAllBaseAnalysisOperators(); - /// Post-hoc dependency resolution: iterates over all operator nodes and - /// matches input field names against other nodes' output field names to - /// populate the Upstreams vectors. This forms the edges of the - /// dependency graph. In future versions, this will be replaced by - /// signature-based deduplication during graph construction. - void buildOperatorDependencies(); - - /// Sets ComputeAlarms on terminal nodes by borrowing alarm pointers from - /// associated IOStream instances. For temporal reduction operators, also - /// creates accumulation alarms and adds them to ComputeAlarms. Then - /// calls propagateAlarmsUpstream() to propagate alarms to upstream - /// dependencies. - void setComputeAlarms(); - - /// Calls initialize() on all operators after the dependency graph is - /// complete and all Fields exist. This allows operators to store - /// pointers to mesh, environment, and other resources needed during - /// compute(). - void initializeAllOps(); - /// Iteratively propagates alarm pointers from downstream operators to /// upstream operators. An upstream operator must be computed whenever /// any of its downstream consumers needs data, so each downstream alarm diff --git a/components/omega/src/analysis/AnalysisGroup.cpp b/components/omega/src/analysis/AnalysisGroup.cpp index c3f9e468310d..5028b55ad462 100644 --- a/components/omega/src/analysis/AnalysisGroup.cpp +++ b/components/omega/src/analysis/AnalysisGroup.cpp @@ -16,6 +16,19 @@ namespace OMEGA { // Returns the name of this AnalysisGroup instance std::string AnalysisGroup::getName() { return GroupName; } // end getName +//------------------------------------------------------------------------------ +// Sets the IOName metadata on an output Field so that IOStream will write +// it under a legible name rather than the full operator-chain string. +// This metadata is purely cosmetic and only affects the netCDF variable name +// in output files - the field lookup system continues to use the internal +// operator-chain name. If the field does not exist, this is a no-op. +void AnalysisGroup::setOutputIOName(const std::string &InternalFieldName, + const std::string &IOName) { + if (Field::exists(InternalFieldName)) { + Field::get(InternalFieldName)->addMetadata("IOName", IOName); + } +} + //------------------------------------------------------------------------------ void AnalysisGroup::parseTemporalPeriods(Config &AnalysisGroupOptions) { Error Err1; @@ -38,13 +51,27 @@ void AnalysisGroup::parseTemporalPeriods(Config &AnalysisGroupOptions) { //------------------------------------------------------------------------------ void AnalysisGroup::buildTemporalChains( const std::vector &ChainStems, Config &AnalysisGroupOptions, - Analysis *AnalysisManager) { + Analysis *AnalysisManager, const std::vector &ChainConfigs) { + + // Validate ChainConfigs size if provided + if (!ChainConfigs.empty() && ChainConfigs.size() != ChainStems.size()) { + ABORT_ERROR("AnalysisGroup::buildTemporalChains: ChainConfigs size ({}) " + "does not match ChainStems size ({})", + ChainConfigs.size(), ChainStems.size()); + } // Parse temporal periods from config parseTemporalPeriods(AnalysisGroupOptions); // Build temporal reduction chains for each stem - for (const auto &StemStr : ChainStems) { + for (size_t i = 0; i < ChainStems.size(); ++i) { + const auto &StemStr = ChainStems[i]; + + // Get config for this chain (empty Config if not provided) + Config ChainConfig; + if (!ChainConfigs.empty()) { + ChainConfig = ChainConfigs[i]; + } // Create temporal reduction chains: Stem -> TimeMean for (const auto &ReductionPeriod : ReductionPeriodList) { @@ -54,15 +81,27 @@ void AnalysisGroup::buildTemporalChains( // Store metadata for stream creation OpChainInfos.push_back(OpChainInfo{ChainStr, ReductionPeriod, true}); - // Parse chain and instantiate operators - AnalysisManager->parseChainAndBuildOps(ChainStr); + // Parse chain and instantiate operators (empty Config is fine) + AnalysisManager->parseChainAndBuildOps(ChainStr, ChainConfig); + + // Apply IOName if specified in config + std::string IOName; + if (ChainConfig.get("IOName", IOName).isSuccess()) { + setOutputIOName(ChainStr, IOName + "_TimeMean" + ReductionPeriod); + } } // Create instantaneous snapshot chains (if requested) if (!SnapshotPeriodList.empty()) { // Parse stem chain if not already built (operators may exist from // reduction chains above, parseChainAndBuildOps handles duplicates) - AnalysisManager->parseChainAndBuildOps(StemStr); + AnalysisManager->parseChainAndBuildOps(StemStr, ChainConfig); + + // Apply IOName if specified in config + std::string IOName; + if (ChainConfig.get("IOName", IOName).isSuccess()) { + setOutputIOName(StemStr, IOName); + } // Store metadata for each snapshot frequency for (const auto &SnapshotPeriod : SnapshotPeriodList) { diff --git a/components/omega/src/analysis/AnalysisGroup.h b/components/omega/src/analysis/AnalysisGroup.h index 272fab6a31ba..294bee874ad0 100644 --- a/components/omega/src/analysis/AnalysisGroup.h +++ b/components/omega/src/analysis/AnalysisGroup.h @@ -81,6 +81,12 @@ class AnalysisGroup { ///< instantaneous output }; + /// Sets the IOName metadata on an output Field so IOStream writes it under + /// a legible name rather than the full operator-chain string. + /// No-op if the field does not exist. + static void setOutputIOName(const std::string &InternalFieldName, + const std::string &IOName); + /// Template for constructing IOStream configurations for this group's /// output. Provides default values for all IOStream creation parameters. /// Derived classes can override defaults using group-specific config options @@ -100,8 +106,8 @@ class AnalysisGroup { {"Precision", "double"}, {"Freq", ""}, {"FreqUnits", ""}, - {"FileFreq", ""}, - {"FileFreqUnits", ""}, + {"FileFreq", "9999"}, + {"FileFreqUnits", "Years"}, {"UseStartEnd", "false"}, {"StartTime", ""}, {"EndTime", ""}, @@ -155,11 +161,18 @@ class AnalysisGroup { /// operator chains by appending time operators to the provided stems, /// calls parseChainAndBuildOps for each chain, and populates OpChainInfos /// with metadata for stream creation. + /// + /// If ChainConfigs is provided and non-empty, it must have the same size + /// as ChainStems, with each Config containing operator-specific parameters + /// for the corresponding chain. If ChainConfigs is empty, chains are built + /// without additional configuration (default behavior). void buildTemporalChains( const std::vector &ChainStems, ///< [in] operator chain stems Config &AnalysisGroupOptions, ///< [in] group configuration - Analysis *AnalysisManager ///< [in] analysis manager + Analysis *AnalysisManager, ///< [in] analysis manager + const std::vector &ChainConfigs = {} + ///< [in] optional per-chain configs ); /// Reads ReductionPeriod and SnapshotPeriod from config and validates diff --git a/components/omega/src/analysis/AnalysisOpFactory.h b/components/omega/src/analysis/AnalysisOpFactory.h index f394f8118e23..c58e5708b34f 100644 --- a/components/omega/src/analysis/AnalysisOpFactory.h +++ b/components/omega/src/analysis/AnalysisOpFactory.h @@ -155,6 +155,134 @@ class AnalysisOpFactory { #undef REGISTER_VARIANT } // end registerAllArrayVariants + /// Registers only 1D array type variants of a templated operator class. + /// Used for operators that only support 1D arrays. Uses SFINAE to prevent + /// template instantiation for non-1D array types at compile time. + + // Helper: enabled only for 1D arrays - registers the operator variant + template