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
1 change: 1 addition & 0 deletions CMake/BuildERFExe.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ function(build_erf_lib erf_lib_name)
${SRC_DIR}/IO/ERF_Write1DProfiles_stag.cpp
${SRC_DIR}/IO/ERF_WriteScalarProfiles.cpp
${SRC_DIR}/IO/ERF_Plotfile.cpp
${SRC_DIR}/IO/ERF_Plotfile2DCatalog.cpp
${SRC_DIR}/IO/ERF_Plotfile2D.cpp
${SRC_DIR}/IO/ERF_Plotfile2DUtils.cpp
${SRC_DIR}/IO/ERF_WriteSubvolume.cpp
Expand Down
3 changes: 3 additions & 0 deletions Docs/sphinx_doc/Plotfiles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,9 @@ concentration.
Output Options for 2D Plotfiles
-------------------------------

The table below lists the built-in 2D diagnostic catalog. ERF writes selected
variables in this order.

+--------------------+---------------------------------------------------------------+
| Parameter | Definition |
+====================+===============================================================+
Expand Down
10 changes: 0 additions & 10 deletions Source/ERF.H
Original file line number Diff line number Diff line change
Expand Up @@ -1228,16 +1228,6 @@ private:
,"qsrc_sw", "qsrc_lw"
};

// **************************************************************************************
// NOTE: The order of variable names here **MUST MATCH THE ORDER** in IO/ERF_Plotfile.cpp
// **************************************************************************************
const amrex::Vector<std::string> derived_names_2d {
"z_surf", "landmask", "mapfac", "lat_m", "lon_m",
"u_star", "w_star", "t_star", "q_star", "Olen", "pblh",
"t_surf", "q_surf", "z0", "OLR", "sens_flux", "laten_flux",
"surf_pres", "integrated_qv"
};

// **************************************************************************************
// NOTE: The order of variable names here **MUST MATCH THE ORDER** in IO/ERF_WriteSubVolume.cpp
// **************************************************************************************
Expand Down
14 changes: 11 additions & 3 deletions Source/IO/ERF_Plotfile2D.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ERF.H"
#include "ERF_Plotfile2DCatalog.H"
#include "ERF_NCPlotFile.H"
#include "ERF_Plotfile2DUtils.H"
#include "ERF_EpochTime.H"
Expand All @@ -16,6 +17,8 @@ namespace
// validates the requested names, assembles the slab geometry, fills existing
// diagnostics, and dispatches to AMReX or NetCDF output. Nontrivial science
// diagnostics should live in dedicated modules and be called from here.
// Keep the fill order below synchronized with plotfile2d::diagnostic_catalog()
// until the fill blocks move into dedicated diagnostic modules.

std::string make_2d_plotfile_name (int which,
int plot_step,
Expand Down Expand Up @@ -102,17 +105,20 @@ ERF::setPlotVariables2D (const std::string& pp_plot_var_names, Vector<std::strin
requested_plot_names.push_back(nm);
}

const auto available_names = plotfile2d::diagnostic_names();

// Keep the canonical built-in 2D ordering so the plotfile component layout
// stays stable even if the input request order changes.
const auto selection = plotfile2d::select_requested_plot_variables(requested_plot_names, derived_names_2d);
const auto selection = plotfile2d::select_requested_plot_variables(requested_plot_names,
available_names);
plot_var_names = selection.accepted;

// Unknown 2D names are skipped rather than aborting because the 2D plot
// list is intentionally user-configurable and may include names that are not
// compiled into a given build. The warning is still explicit so the user
// can correct the input deck.
warn_for_unavailable_2d_plot_vars(plotfile2d::format_plot2d_parameter_name(pp_prefix, pp_plot_var_names),
selection.unavailable, derived_names_2d);
selection.unavailable, available_names);
}

void
Expand Down Expand Up @@ -464,11 +470,13 @@ ERF::Write2DPlotFile (int which, PlotFileType plotfile_type, Vector<std::string>
mf_comp++;
} // sens_flux

// Keep the legacy output name "laten_flux"; it maps to the vertical
// water-vapor surface flux field.
if (containerHasElement(plot_var_names, "laten_flux")) {
#ifdef _OPENMP
#pragma omp parallel if (amrex::Gpu::notInLaunchRegion())
#endif
if (SFS_hfx3_lev[lev]) {
if (SFS_q1fx3_lev[lev]) {
for ( MFIter mfi(mf[lev],TilingIfNotGPU()); mfi.isValid(); ++mfi)
{
const Box& bx = mfi.tilebox();
Expand Down
69 changes: 69 additions & 0 deletions Source/IO/ERF_Plotfile2DCatalog.H
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#ifndef ERF_PLOTFILE2DCATALOG_H_
#define ERF_PLOTFILE2DCATALOG_H_

#include <string>

#include <AMReX_Vector.H>

namespace plotfile2d
{

enum class DiagnosticID
{
ZSurf,
LandMask,
MapFac,
LatM,
LonM,
UStar,
WStar,
TStar,
QStar,
Olen,
Pblh,
TSurf,
QSurf,
Z0,
OLR,
SensFlux,
LatenFlux,
SurfPres,
IntegratedQv
};

enum class DiagnosticCategory
{
Geometry,
SurfaceLayer,
Radiation,
SurfaceFlux,
SurfaceState,
ColumnIntegral
};

enum class MissingPolicy
{
AlwaysAvailable,
FillZeroWhenUnavailable,
FillMinus999WhenUnavailable
};

struct DiagnosticDescriptor
{
DiagnosticID id;
const char* name;
const char* long_name;
const char* units;
DiagnosticCategory category;
MissingPolicy missing_policy;
};

const amrex::Vector<DiagnosticDescriptor>& diagnostic_catalog ();

amrex::Vector<std::string> diagnostic_names ();

const DiagnosticDescriptor* find_diagnostic (const std::string& name);

} // namespace plotfile2d

#endif
77 changes: 77 additions & 0 deletions Source/IO/ERF_Plotfile2DCatalog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#include "ERF_Plotfile2DCatalog.H"

namespace plotfile2d
{

// This catalog is the source of truth for built-in 2D plotfile names and
// metadata. Its order defines the canonical component order used for user
// selection. Keep this order synchronized with the fill blocks in
// ERF_Plotfile2D.cpp until those fill blocks move to dedicated diagnostic
// modules.
// The catalog is metadata only. It must not become a home for diagnostic
// science.

namespace
{

const amrex::Vector<DiagnosticDescriptor>& catalog_storage ()
{
static const amrex::Vector<DiagnosticDescriptor> catalog{
{DiagnosticID::ZSurf, "z_surf", "Surface elevation", "m", DiagnosticCategory::Geometry, MissingPolicy::AlwaysAvailable},
{DiagnosticID::LandMask, "landmask", "Land-sea mask", "1", DiagnosticCategory::Geometry, MissingPolicy::AlwaysAvailable},
{DiagnosticID::MapFac, "mapfac", "Map factor at mass points", "1", DiagnosticCategory::Geometry, MissingPolicy::AlwaysAvailable},
{DiagnosticID::LatM, "lat_m", "Latitude at unstaggered mass points", "deg", DiagnosticCategory::Geometry, MissingPolicy::FillZeroWhenUnavailable},
{DiagnosticID::LonM, "lon_m", "Longitude at unstaggered mass points", "deg", DiagnosticCategory::Geometry, MissingPolicy::FillZeroWhenUnavailable},
{DiagnosticID::UStar, "u_star", "Friction velocity from the surface layer", "m/s", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::WStar, "w_star", "Convective velocity scale from the surface layer","m/s", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::TStar, "t_star", "Temperature scale from the surface layer", "K", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::QStar, "q_star", "Humidity scale from the surface layer", "kg/kg", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::Olen, "Olen", "Obukhov length from the surface layer", "m", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::Pblh, "pblh", "Planetary boundary layer height", "m", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::TSurf, "t_surf", "Surface temperature from the surface layer", "K", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::QSurf, "q_surf", "Surface humidity from the surface layer", "kg/kg", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::Z0, "z0", "Roughness height from the surface layer", "m", DiagnosticCategory::SurfaceLayer, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::OLR, "OLR", "Outgoing longwave radiation at the model top", "W/m^2", DiagnosticCategory::Radiation, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::SensFlux, "sens_flux", "Surface sensible heat flux", "kg K m^-2 s^-1", DiagnosticCategory::SurfaceFlux, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::LatenFlux, "laten_flux", "Surface moisture flux (legacy output name)", "kg m^-2 s^-1", DiagnosticCategory::SurfaceFlux, MissingPolicy::FillMinus999WhenUnavailable},
{DiagnosticID::SurfPres, "surf_pres", "Surface pressure", "Pa", DiagnosticCategory::SurfaceState, MissingPolicy::AlwaysAvailable},
{DiagnosticID::IntegratedQv, "integrated_qv","Column-integrated water vapor", "kg/m^2", DiagnosticCategory::ColumnIntegral, MissingPolicy::FillZeroWhenUnavailable},
};

return catalog;
}

} // namespace

const amrex::Vector<DiagnosticDescriptor>&
diagnostic_catalog ()
{
return catalog_storage();
}

amrex::Vector<std::string>
diagnostic_names ()
{
amrex::Vector<std::string> names;
names.reserve(diagnostic_catalog().size());

for (const auto& descriptor : diagnostic_catalog()) {
names.push_back(descriptor.name);
}

return names;
}

const DiagnosticDescriptor*
find_diagnostic (const std::string& name)
{
for (const auto& descriptor : diagnostic_catalog()) {
if (name == descriptor.name) {
return &descriptor;
}
}

return nullptr;
}

} // namespace plotfile2d
1 change: 1 addition & 0 deletions Source/IO/Make.package
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

CEXE_sources += ERF_Plotfile.cpp
CEXE_sources += ERF_Plotfile2DCatalog.cpp
CEXE_sources += ERF_Plotfile2D.cpp
CEXE_sources += ERF_Plotfile2DUtils.cpp
CEXE_sources += ERF_Checkpoint.cpp
Expand Down
108 changes: 108 additions & 0 deletions Tests/Unit/IO/ERF_GTestPlotfile2D.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <string>
#include <unordered_set>

#include <gtest/gtest.h>

#include "ERF_Plotfile2DCatalog.H"
#include "ERF_Plotfile2DUtils.H"

using namespace plotfile2d;
Expand Down Expand Up @@ -88,6 +90,112 @@ TEST(Plotfile2D, EmptyRequestsReturnEmptyLists)
EXPECT_TRUE(selection.unavailable.empty());
}

// Motivation: The catalog order defines the canonical 2D plotfile layout, so
// this test catches accidental reordering before it changes output metadata.
TEST(Plotfile2D, CatalogNamesMatchCanonicalOrder)
{
const amrex::Vector<std::string> expected{
"z_surf", "landmask", "mapfac", "lat_m", "lon_m",
"u_star", "w_star", "t_star", "q_star", "Olen", "pblh",
"t_surf", "q_surf", "z0", "OLR", "sens_flux", "laten_flux",
"surf_pres", "integrated_qv"
};

EXPECT_EQ(plotfile2d::diagnostic_names(), expected);
}

// Motivation: Each built-in 2D diagnostic name must be unique so user input
// maps to one output component and one metadata record.
TEST(Plotfile2D, CatalogNamesAreUnique)
{
const auto names = plotfile2d::diagnostic_names();
std::unordered_set<std::string> unique_names(names.begin(), names.end());

EXPECT_EQ(unique_names.size(), names.size());
}

// Motivation: Each built-in 2D diagnostic ID must be unique so the catalog can
// stay stable even if a display name changes later.
TEST(Plotfile2D, CatalogIdsAreUnique)
{
std::unordered_set<int> unique_ids;
for (const auto& descriptor : plotfile2d::diagnostic_catalog()) {
unique_ids.insert(static_cast<int>(descriptor.id));
}

EXPECT_EQ(unique_ids.size(), plotfile2d::diagnostic_catalog().size());
}

// Motivation: Catalog metadata feeds documentation and future diagnostics, so
// each built-in entry needs a name, long name, units, category, and policy.
TEST(Plotfile2D, CatalogDescriptorsHaveRequiredMetadata)
{
for (const auto& descriptor : plotfile2d::diagnostic_catalog()) {
EXPECT_NE(descriptor.name, nullptr);
EXPECT_NE(descriptor.long_name, nullptr);
EXPECT_NE(descriptor.units, nullptr);
EXPECT_FALSE(std::string(descriptor.name).empty());
EXPECT_FALSE(std::string(descriptor.long_name).empty());
EXPECT_FALSE(std::string(descriptor.units).empty());

bool valid_category = false;
switch (descriptor.category) {
case plotfile2d::DiagnosticCategory::Geometry:
case plotfile2d::DiagnosticCategory::SurfaceLayer:
case plotfile2d::DiagnosticCategory::Radiation:
case plotfile2d::DiagnosticCategory::SurfaceFlux:
case plotfile2d::DiagnosticCategory::SurfaceState:
case plotfile2d::DiagnosticCategory::ColumnIntegral:
valid_category = true;
break;
}
EXPECT_TRUE(valid_category);

bool valid_missing_policy = false;
switch (descriptor.missing_policy) {
case plotfile2d::MissingPolicy::AlwaysAvailable:
case plotfile2d::MissingPolicy::FillZeroWhenUnavailable:
case plotfile2d::MissingPolicy::FillMinus999WhenUnavailable:
valid_missing_policy = true;
break;
}
EXPECT_TRUE(valid_missing_policy);
}
}

// Motivation: The selection helper should be able to resolve a known catalog
// name back to the descriptor that defines its metadata.
TEST(Plotfile2D, FindDiagnosticReturnsDescriptorForKnownName)
{
const auto* descriptor = plotfile2d::find_diagnostic("sens_flux");

ASSERT_NE(descriptor, nullptr);
EXPECT_STREQ(descriptor->name, "sens_flux");
EXPECT_EQ(descriptor->id, plotfile2d::DiagnosticID::SensFlux);
EXPECT_EQ(descriptor->category, plotfile2d::DiagnosticCategory::SurfaceFlux);
EXPECT_EQ(descriptor->missing_policy, plotfile2d::MissingPolicy::FillMinus999WhenUnavailable);
}

// Motivation: Unknown catalog lookups should fail cleanly so callers can
// distinguish a typo from a valid built-in diagnostic.
TEST(Plotfile2D, FindDiagnosticReturnsNullForUnknownName)
{
EXPECT_EQ(plotfile2d::find_diagnostic("not_a_real_2d_name"), nullptr);
}

// Motivation: The selection helper must follow the catalog order, not the user
// request order, so equivalent input decks produce the same component layout.
TEST(Plotfile2D, SelectionUsesCatalogCanonicalOrder)
{
const auto available = plotfile2d::diagnostic_names();
const amrex::Vector<std::string> requested{"surf_pres", "z0", "z_surf", "lon_m"};

const auto selection = select_requested_plot_variables(requested, available);

const amrex::Vector<std::string> expected{"z_surf", "lon_m", "z0", "surf_pres"};
EXPECT_EQ(selection.accepted, expected);
}

// Motivation: A warning must name both the input parameter and the skipped
// variable so the user can fix the correct namelist entry.
TEST(Plotfile2D, WarningMessageNamesParameterAndVariable)
Expand Down
Loading