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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions components/eamxx/docs/user/io_aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# EAMxx Field Aliasing Feature

This document demonstrates the field aliasing feature for EAMxx I/O operations.

## Overview

The field aliasing feature allows users to specify custom names for
variables in netcdf output files while maintaining the original
internal field names in the model. This is useful for:

- Creating shorter, more convenient variable names for output
- Maintaining compatibility with external tools that expect specific variable names
- Providing user-friendly names for complex internal field names

## Syntax

The alias syntax uses the delimiter `:=` to separate the alias name
from the internal field name:

```yaml
alias_name:=internal_field_name
```

## YAML Configuration Examples

### Basic Usage

```yaml
field_names:
- "LWP:=LiqWaterPath" # Alias LWP for LiqWaterPath
- "SWP:=SolidWaterPath" # Alias SWP for SolidWaterPath
- "T:=T_mid" # Alias T for T_mid
- "qv" # Regular field name (no alias)
```

### Mixed Usage

You can mix aliased and non-aliased fields in the same configuration:

```yaml
field_names:
- "T_mid" # Regular field name
- "LWP:=LiqWaterPath" # Aliased field
- "p_mid" # Regular field name
- "RH:=RelativeHumidity" # Aliased field
```

## Output Behavior

When using aliases:

1. **NetCDF Variables**: The netcdf file will contain variables
named according to the aliases

- `LWP` instead of `LiqWaterPath`
- `T` instead of `T_mid`
- `RH` instead of `RelativeHumidity`

2. **Internal Processing**: All internal model operations use the
original field names

- Field validation uses `LiqWaterPath`, `T_mid`, etc.
- Diagnostic calculations use original names
- Memory management uses original field structures

3. **Metadata**: Variable attributes (units, long_name, etc.)
are preserved from the original fields, and `eamxx_name`
is added to the netcdf files to document aliasing

## Caveats

Currently, a field can be requested only once in a single stream,
and either the original name or the alias name counts.

```yaml
field_names:
- "LWP:=" # Error: empty field name
- ":=LiqWaterPath" # Error: empty alias name
- "LWP:=Field1" # OK
- "LWP:=Field2" # Error: duplicate alias LWP
- "LWP1:=LiqWaterPath" # OK
- "LWP2:=LiqWaterPath" # Error: duplicate field LiqWaterPath
```
1 change: 1 addition & 0 deletions components/eamxx/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ nav:
- 'Overview': 'user/diags/index.md'
- 'Field contraction diagnostics': 'user/diags/field_contraction.md'
- 'Presentations': 'user/presentations.md'
- 'IO Aliases': 'user/io_aliases.md'
- 'Developer Guide':
- 'Quick-start Guide': 'developer/dev_quickstart.md'
- 'Code Structure and Organization': 'developer/code_structure.md'
Expand Down
50 changes: 50 additions & 0 deletions components/eamxx/src/share/io/eamxx_io_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "share/util/eamxx_utils.hpp"
#include "share/eamxx_config.hpp"

#include <ekat_string_utils.hpp>

#include <fstream>
#include <regex>

Expand Down Expand Up @@ -231,4 +233,52 @@ create_diagnostic (const std::string& diag_field_name,
return diag;
}

std::pair<std::string, std::string>
parse_field_alias (const std::string& field_spec)
{
const std::string delimiter = ":=";
Copy link
Contributor

Choose a reason for hiding this comment

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

The first part of this fcn could be replaced with

  auto tokens = ekat::split(field_spec,":=");
  EKAT_REQUIRE_MSG (tokens.size()<=2,
    "Error! Invalid field alias specification: '" + field_spec + "'\n"
    "Multiple ':=' tokens found.\n");

  auto alias = tokens[0];
  auto field_name = tokens[1];

Copy link
Contributor

Choose a reason for hiding this comment

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

what if field_spec has no :=?

Should I just add this check at the end?

  if (pos == std::string::npos) {
    // No alias found, return the field_spec as both alias and field name
    std::string trimmed = ekat::trim(field_spec);
    return {trimmed, trimmed};
  }

  // Extract and trim alias and field name
  std::string alias = ekat::trim(field_spec.substr(0, pos));
  std::string field_name = ekat::trim(field_spec.substr(pos + delimiter.length()));

  EKAT_REQUIRE_MSG(!alias.empty() && !field_name.empty(),
      "Error! Invalid field alias specification: '" + field_spec + "'\n"
      "Expected format: 'alias:=field_name' where both alias and field_name are non-empty.\n");

+ auto another_pos = field_name.find(delimiter);
+ if (another_pos == std::string::npos){
  return {alias, field_name};
+ } else {
+ EKAT_ERROR_MSG(
+   "Error! Invalid field alias specification: '" + field_spec + "'\n"
+   "Multiple ':=' tokens found.\n");
+ );
  }

Copy link
Contributor

@bartgol bartgol Aug 4, 2025

Choose a reason for hiding this comment

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

ekat::split would simply put the whole input string in tokens[0], so the suggestion above may help with that too.

Edit: nvm, my code is wrong. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, if tokens.size()==1, we should just return {ekat::trim(tokens[0]), ekat::trim(tokens[0]}).

Copy link
Contributor

Choose a reason for hiding this comment

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

So, if there's a second := in the string, it would fail with not found in the list of fields/diags. I think that's good enough to handle that error. We can improve on that later.

Anything else you see blocking this PR?

Copy link
Contributor

@bartgol bartgol Aug 4, 2025

Choose a reason for hiding this comment

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

In the version I posted in my comment, it should throw an exception since tokens.size()>2.

Edit: so, yeah, currently it would fail since the RHS would be not found as a diag.

auto pos = field_spec.find(delimiter);

if (pos == std::string::npos) {
// No alias found, return the field_spec as both alias and field name
std::string trimmed = ekat::trim(field_spec);
return {trimmed, trimmed};
}

// Extract and trim alias and field name
std::string alias = ekat::trim(field_spec.substr(0, pos));
std::string field_name = ekat::trim(field_spec.substr(pos + delimiter.length()));

EKAT_REQUIRE_MSG(!alias.empty() && !field_name.empty(),
"Error! Invalid field alias specification: '" + field_spec + "'\n"
"Expected format: 'alias:=field_name' where both alias and field_name are non-empty.\n");

auto another_pos = field_name.find(delimiter);
EKAT_REQUIRE_MSG(another_pos == std::string::npos,
"Error! Invalid field alias specification: '" + field_spec + "'\n"
"Multiple ':=' tokens found.\n");
return {alias, field_name};
}

std::pair<std::map<std::string, std::string>, std::vector<std::string>>
process_field_aliases (const std::vector<std::string>& field_specs)
{
std::map<std::string, std::string> alias_to_field_map;
std::vector<std::string> alias_names;

for (const auto& spec : field_specs) {
auto [alias, field_name] = parse_field_alias(spec);

// Check for duplicate aliases
EKAT_REQUIRE_MSG(alias_to_field_map.find(alias) == alias_to_field_map.end(),
"Error! Duplicate field alias found: '" + alias + "'\n"
"Each alias must be unique within the field list.\n");

alias_to_field_map[alias] = field_name;
alias_names.push_back(alias);
}

return {alias_to_field_map, alias_names};
}

} // namespace scream
11 changes: 11 additions & 0 deletions components/eamxx/src/share/io/eamxx_io_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,16 @@ std::shared_ptr<AtmosphereDiagnostic>
create_diagnostic (const std::string& diag_name,
const std::shared_ptr<const AbstractGrid>& grid);

// Parse field alias from field specification string.
// Format: "alias:=field_name" returns {alias, field_name}
// For non-aliased fields, returns {field_name, field_name}
std::pair<std::string, std::string>
parse_field_alias (const std::string& field_spec);

// Process a list of field specifications with potential aliases.
// Returns a map from alias_name -> internal_field_name and a vector of alias names
std::pair<std::map<std::string, std::string>, std::vector<std::string>>
process_field_aliases (const std::vector<std::string>& field_specs);

} // namespace scream
#endif // SCREAM_IO_UTILS_HPP
Loading
Loading