From e8085ac9d2a00a50348c313f23f540c8cacde92f Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 25 Jun 2026 13:23:00 -0700 Subject: [PATCH 01/27] Build out basic implementation of SfcCoupling --- components/omega/src/ocn/SfcCoupling.cpp | 148 +++++++++++++++ components/omega/src/ocn/SfcCoupling.h | 137 ++++++++++++++ components/omega/test/CMakeLists.txt | 12 ++ components/omega/test/ocn/SfcCouplingTest.cpp | 175 ++++++++++++++++++ 4 files changed, 472 insertions(+) create mode 100644 components/omega/src/ocn/SfcCoupling.cpp create mode 100644 components/omega/src/ocn/SfcCoupling.h create mode 100644 components/omega/test/ocn/SfcCouplingTest.cpp diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp new file mode 100644 index 000000000000..2470e6a96964 --- /dev/null +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -0,0 +1,148 @@ +#include "SfcCoupling.h" +#include "Logging.h" + +namespace OMEGA { + +// create the static class member +SfcCoupling *SfcCoupling::DefaultSfcCoupling = nullptr; +std::map> SfcCoupling::AllSfcCoupling; + +// Initalize the surface coupling. Assumes the ... have been initialized +int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { + + int Err = 0; // default successful return code + + // Retrieve the default horizontal mesh and timestepper + HorzMesh *DefHorzMesh = HorzMesh::getDefault(); + auto *DefTimeStepper = TimeStepper::getDefault(); + + TimeInterval OcnTimeStep = DefTimeStepper->getTimeStep(); + TimeInterval CplTimeStep = CouplingInitParams.CouplingTimeStep; + + R8 CplTimeStepSeconds, OcnTimeStepSeconds; + CplTimeStep.get(CplTimeStepSeconds, TimeUnits::Seconds); + OcnTimeStep.get(OcnTimeStepSeconds, TimeUnits::Seconds); + + if (CplTimeStepSeconds < OcnTimeStepSeconds) { + LOG_ERROR("Coupling interval is: {} (seconds)", CplTimeStepSeconds); + LOG_ERROR("Ocean timestep is: {} (seconds)", OcnTimeStepSeconds); + ABORT_ERROR( + "The ocean timestep cannot be longer than the coupling interval."); + } + + if (std::fmod(CplTimeStepSeconds, OcnTimeStepSeconds) > 1e-10) { + LOG_ERROR("Coupling interval is: {} (seconds)", CplTimeStepSeconds); + LOG_ERROR("Ocean timestep is: {} (seconds)", OcnTimeStepSeconds); + ABORT_ERROR( + "Coupling interval must be evenly divisible by the ocean timestep."); + } + + // Create the default surface coupling object and set pointer to it + SfcCoupling::DefaultSfcCoupling = + SfcCoupling::create("Default", DefHorzMesh, CouplingInitParams.ImportIdx, + CouplingInitParams.ExportIdx, DefTimeStepper, + CplTimeStep, CouplingInitParams.Layout); + + return Err; +} + +// Construct a new surface coupling object +SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, + const std::map &ImportIdx, + const std::map &ExportIdx, + TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, + const CouplingLayout &Layout) + : Name(Name_), ImportIdx(ImportIdx), ExportIdx(ExportIdx), + CplToOcn(Name_, Mesh), OcnToCpl(Name_, Mesh), Layout(Layout) { + + // Retrieve mesh cell count + NCellsOwned = Mesh->NCellsOwned; + + NAccumSteps = 0; + + // Allocate variables on stack for creating the CouplingAlarm + std::string AlarmName = "CouplingAlarm"; + Clock *StepperClock = Stepper->getClock(); + TimeInstant StartTime = Stepper->getStartTime(); + + // Create a CouplingAlarm associated with CouplingTimeStep + if (Name_ != "Default") + AlarmName += Name_; + + CouplingAlarm = Alarm(AlarmName, CouplingTimeStep, StartTime); + StepperClock->attachAlarm(&CouplingAlarm); +} + +// Create a new surface coupling object by calling the constructor and storing +// it in the AllSfcCoupling map +SfcCoupling *SfcCoupling::create(const std::string &Name, const HorzMesh *Mesh, + const std::map &ImportIdx, + const std::map &ExportIdx, + TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, + const CouplingLayout &Layout) { + + // Check to see if a surface coupling of the same name already exists + if (AllSfcCoupling.find(Name) != AllSfcCoupling.end()) { + LOG_ERROR("Attempted to create a SfcCoupling with name {}, but a " + "SfcCoupling with that name already exists", + Name); + return nullptr; + } + + // create a new surface coupling on the heap and store it in the map of + // unique_ptrs, which will manage its lifetime + auto *NewSfcCoupling = new SfcCoupling(Name, Mesh, ImportIdx, ExportIdx, + Stepper, CouplingTimeStep, Layout); + AllSfcCoupling.emplace(Name, NewSfcCoupling); + + return NewSfcCoupling; +} // end SfcCoupling create + +// Get the default surface coupling object +SfcCoupling *SfcCoupling::getDefault() { + return SfcCoupling::DefaultSfcCoupling; +} + +// Get a surface coupling object by name +SfcCoupling *SfcCoupling::get(const std::string Name) { + // look for an instance of this name + auto it = AllSfcCoupling.find(Name); + + // if found, return the pointer + if (it != AllSfcCoupling.end()) { + return it->second.get(); + + // otherwise print error message and return nullptr + } else { + LOG_ERROR("SfcCoupling::get: Attempted to retrieve non-existent " + "surface coupling object:"); + LOG_ERROR("{} has not been defined or has been removed", Name); + return nullptr; + } +} + +// Destructor +SfcCoupling::~SfcCoupling() {} + +// Remove surface coupling object by name +void SfcCoupling::erase(const std::string Name) { AllSfcCoupling.erase(Name); } + +// Remove all surface coupling objects +void SfcCoupling::clear() { + AllSfcCoupling.clear(); + DefaultSfcCoupling = nullptr; // prevent dangling pointer +} + +CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) + : SurfaceStressZonal("SurfaceStressZonal" + Suffix, Mesh->NCellsOwned), + SurfaceStressMeridional("SurfaceStressMeridional" + Suffix, + Mesh->NCellsOwned) {} + +OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) + : SurfaceTemperature("SurfaceTemperature" + Suffix, Mesh->NCellsOwned), + SurfaceVelocityZonal("SurfaceVelocityZonal" + Suffix, Mesh->NCellsOwned), + SurfaceVelocityMeridional("SurfaceVelocityMeridional" + Suffix, + Mesh->NCellsOwned) {} +} // namespace OMEGA diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h new file mode 100644 index 000000000000..896e2453601f --- /dev/null +++ b/components/omega/src/ocn/SfcCoupling.h @@ -0,0 +1,137 @@ +#ifndef OMEGA_SURFACECOUPLING_H +#define OMEGA_SURFACECOUPLING_H +//===-- ocn/SurfaceCouling.h - surface coupling ----------------*- C++ -*-===// +// +/// \file +/// \brief Contains the coupling variables exchanged with the coupler +/// +/// The SurfaceCouling class contains the variables exchanged with the coupler +/// for a sub-domain of the global horizontal mesh. +// +//===----------------------------------------------------------------------===// + +#include "DataTypes.h" +#include "HorzMesh.h" +#include "TimeMgr.h" +#include "TimeStepper.h" + +#include + +namespace OMEGA { + +enum class CouplingLayout { MCT, MOAB }; + +// Parameters needed to initialize a SfcCoupling object. The information +// needed to initialize these parameters is provided by the coupler. +struct CouplingInitParams { + std::map ImportIdx; + std::map ExportIdx; + TimeInterval CouplingTimeStep; + CouplingLayout Layout; +}; + +// x2o: Coupler to Ocean +class CplToOcnFields { + public: + // x2o fields only need to be stored on the host. + // The SfcCoupling::applyImportFields() method will handle copying the + // data to the device. + HostArray1DReal SurfaceStressZonal; ///< Foxx_taux [N m^-2] + HostArray1DReal SurfaceStressMeridional; ///< Foxx_tauy [N m^-2] + + CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh); +}; + +// o2x: Ocean to Coupler +class OcnToCplFields { + public: + ///< So_t [deg C] + Array1DReal SurfaceTemperature; + HostArray1DReal SurfaceTemperature_H; + + ///< So_u [m s^-1] + Array1DReal SurfaceVelocityZonal; + HostArray1DReal SurfaceVelocityZonal_H; + + ///< So_v [m s^-1] + Array1DReal SurfaceVelocityMeridional; + HostArray1DReal SurfaceVelocityMeridional_H; + + OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); +}; + +/// A class for interfacing with the coupler + +/// The SfcCoupling class provides a container for the variables exchanged +/// to (o2x) and from (x2o) the coupler. It containes methods to handle the +/// import and export of raw data from the coupler, unit conversion of said +/// data, application of that data to the model state, and accumulation of the +/// data for avergaing or sumation over multiple ocean time steps. +class SfcCoupling { + + private: + static SfcCoupling *DefaultSfcCoupling; + + static std::map> AllSfcCoupling; + + // Construct a new local coupling object + SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, + const std::map &ImportIdx, + const std::map &ExportIdx, + TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, + const CouplingLayout &Layout); + + // Forbid copy and move construction + SfcCoupling(const SfcCoupling &) = delete; + SfcCoupling(SfcCoupling &&) = delete; + + // Number of ocn timesteps acccumulated over the coupling interval + I4 NAccumSteps; + + CouplingLayout Layout; ///< Coupling layout (MCT or MOAB) + + // Map of import/export variable names to index in the raw data arrays + std::map ImportIdx; + std::map ExportIdx; + + public: + std::string Name; + + I4 NCellsOwned; ///< Number of cells owned by this task + + // Coupling Variable containers + CplToOcnFields CplToOcn; ///< Coupler to Ocean (x2o) + OcnToCplFields OcnToCpl; ///< Ocean to Coupler (o2x) + + Alarm CouplingAlarm; ///< Alarm for coupling interval + + // Methods + + /// Create a new surface coupling by calling the constructor and put it + /// in the AllSfcCoupling map + static SfcCoupling *create(const std::string &Name, const HorzMesh *Mesh, + const std::map &ImportIdx, + const std::map &ExportIdx, + TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, + const CouplingLayout &CouplingLayout); + + /// Initlaize SfcCoupling + static int init(const CouplingInitParams &CouplingInitParams); + + /// Destructor - deallocates all memory and deletes an SfcCoupling + ~SfcCoupling(); + + /// Dealllocates arrays + static void clear(); + + /// Remove surface coupling object by name + static void erase(const std::string InName); + + static SfcCoupling *getDefault(); + + static SfcCoupling *get(const std::string name); +}; + +} // end namespace OMEGA +#endif // defined OMEGA_SURFACECOUPLING_H diff --git a/components/omega/test/CMakeLists.txt b/components/omega/test/CMakeLists.txt index b30760fea72b..62bca3fc945d 100644 --- a/components/omega/test/CMakeLists.txt +++ b/components/omega/test/CMakeLists.txt @@ -555,3 +555,15 @@ add_omega_test( ocn/StateValidationTest.cpp "-n;8" ) + +##################### +# SurfaceCouling test +##################### + +# TODO: no halo exhange. Worth testnig with more than 1 task? +add_omega_test( + SFCCOUPLING_TEST + testSfcCoupling.exe + ocn/SfcCouplingTest.cpp + "-n;1" +) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp new file mode 100644 index 000000000000..990905102ed9 --- /dev/null +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -0,0 +1,175 @@ +#include "SfcCoupling.h" +#include "Config.h" +#include "DataTypes.h" +#include "Decomp.h" +#include "Eos.h" +#include "Field.h" +#include "Halo.h" +#include "HorzMesh.h" +#include "IO.h" +#include "IOStream.h" +#include "Logging.h" +#include "MachEnv.h" +#include "OmegaKokkos.h" +#include "Pacer.h" +#include "TimeStepper.h" +#include "VertCoord.h" +#include "mpi.h" + +using namespace OMEGA; + +struct TestSetup { + + std::map ImportIdx = { + {"Foxx_taux", 0}, {"Foxx_tauy", 1}, {"Foxx_swnet", 2}, + {"Foxx_lwnet", 3}, {"Foxx_lat", 4}, {"Foxx_sen", 5}, + {"Foxx_lwup", 6}, {"Faxa_lwdn", 7}, {"Fioi_melth", 8}, + {"Fioi_bergh", 9}, {"Faxa_snow", 10}, {"Faxa_rain", 11}, + {"Foxx_evap", 12}, {"Fioi_meltw", 13}, {"Fioi_bergw", 14}, + {"Fioi_salt", 15}, {"Foxx_rofl", 16}, {"Foxx_rofi", 17}, + {"Si_ifrac", 18}, {"Si_bpress", 19}, {"Sa_pslv", 20}}; + + std::map ExportIdx = { + {"So_t", 0}, {"So_s", 1}, {"So_u", 2}, {"So_v", 3}, + {"So_ssh", 4}, {"So_dhdx", 5}, {"So_dhdy", 6}, {"Fioo_q", 7}, + {"Fioo_frazil", 8}, {"Faoo_h2otemp", 9}}; +}; + +int initRawData() { + int Err = 0; + + return Err; +} + +int initSfcCouplingTest(const std::string &MeshFile) { + + int Err = 0; + + MachEnv::init(MPI_COMM_WORLD); + MachEnv *DefEnv = MachEnv::getDefault(); + MPI_Comm DefComm = DefEnv->getComm(); + + initLogging(DefEnv); + LOG_INFO("------ Surface Coupling unit tests ------"); + + // Open config file + Config("Omega"); + Config::readAll("omega.yml"); + + // Initialize time stepper and get model clock + TimeStepper::init1(); + TimeStepper *DefStepper = TimeStepper::getDefault(); + Clock *ModelClock = DefStepper->getClock(); + + IO::init(DefComm); + Decomp::init(MeshFile); + + // Initialize streams + Field::init(ModelClock); + IOStream::init(ModelClock); + + // TODO: Need to initialize halo? SfcCoupling has no lateral exchanges + int HaloErr = Halo::init(); + if (HaloErr != 0) { + Err++; + LOG_ERROR("SfcCouplingTest: Error initializing defualt halo"); + } + + HorzMesh::init(ModelClock); + + VertCoord::init(); + + int StateErr = OceanState::init(); + if (StateErr != 0) { + Err++; + LOG_ERROR("SfcCouplingTest: Error initializing defualt ocean state"); + } + + Eos::init(); + + return Err; +} + +int testSfcCoupling() { + + int Err = 0; + + TestSetup Setup; + + CouplingInitParams CouplingParams; + CouplingParams.ImportIdx = Setup.ImportIdx; + CouplingParams.ExportIdx = Setup.ExportIdx; + CouplingParams.CouplingTimeStep = TimeInterval(3600.0, TimeUnits::Seconds); + CouplingParams.Layout = CouplingLayout::MCT; + + Err += SfcCoupling::init(CouplingParams); + + // test retrival of default + SfcCoupling *DefCoupling = SfcCoupling::getDefault(); + + if (DefCoupling) { + LOG_INFO("SfcCouplingTest: Default retrival PASS"); + } else { + Err++; + LOG_ERROR("SfcCouplingTest: Default retrival FAIL"); + return -1; + } + + SfcCoupling::clear(); + + return Err; +} +void finalizeSfcCouplingTest() { + + OceanState::clear(); + VertCoord::clear(); + HorzMesh::clear(); + Field::clear(); + Dimension::clear(); + TimeStepper::clear(); + Halo::clear(); + Decomp::clear(); + MachEnv::removeAll(); +} + +int surfaceCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { + + int Err = initSfcCouplingTest(MeshFile); + + if (Err != 0) { + LOG_CRITICAL("SfcCouplingTest: Error initializing"); + } + + Err += testSfcCoupling(); + + if (Err == 0) { + LOG_INFO("SfcCouplingTest: Successful completion"); + } + + finalizeSfcCouplingTest(); + + return Err; +} + +int main(int argc, char *argv[]) { + + int RetVal = 0; + + MPI_Init(&argc, &argv); + Kokkos::initialize(argc, argv); + Pacer::initialize(MPI_COMM_WORLD); + Pacer::setPrefix("Omega:"); + + RetVal += surfaceCouplingTest(); + + Pacer::finalize(); + Kokkos::finalize(); + MPI_Finalize(); + + if (RetVal >= 256) + RetVal = 255; + if (RetVal == 0) + LOG_INFO("------ SufaceCoupling unit tests successful ------"); + + return RetVal; +} // end of main From 1fb44ed7e281728de28f24fa369f79ae4ef739b0 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 25 Jun 2026 19:38:56 -0700 Subject: [PATCH 02/27] Add attachData and importFromCoupler methods --- components/omega/src/ocn/SfcCoupling.cpp | 59 +++++++++++++ components/omega/src/ocn/SfcCoupling.h | 21 ++++- components/omega/test/ocn/SfcCouplingTest.cpp | 83 ++++++++++++++----- 3 files changed, 143 insertions(+), 20 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 2470e6a96964..9edffe3c9353 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -58,6 +58,9 @@ SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, // Retrieve mesh cell count NCellsOwned = Mesh->NCellsOwned; + // Retrieve import/export field counts + NImportFields = ImportIdx.size(); + NExportFields = ExportIdx.size(); NAccumSteps = 0; @@ -135,6 +138,62 @@ void SfcCoupling::clear() { DefaultSfcCoupling = nullptr; // prevent dangling pointer } +// Create views of the raw coupling data arrays +void SfcCoupling::attachData(const Real *CplToOcnData, Real *OcnToCplData) { + + // Kokkos::LayoutStride index math uses a runtime stride value, rather than + // a compile-time-optimized stride value. Can switch to ifdefs if this + // becomes a performance bottleneck + Kokkos::LayoutStride CplToOcnLayout, OcnToCplLayout; + + if (Layout == CouplingLayout::MCT) { + /// MCT layout: (NCellsOwned, NImportFields) - field idx strides faster + CplToOcnLayout = + Kokkos::LayoutStride(NImportFields, 1, NCellsOwned, NImportFields); + OcnToCplLayout = + Kokkos::LayoutStride(NExportFields, 1, NCellsOwned, NImportFields); + } else if (Layout == CouplingLayout::MOAB) { + /// MOAB layout: (NImportFields, NCellsOwned) - cell idx strides faster + CplToOcnLayout = + Kokkos::LayoutStride(NImportFields, NCellsOwned, NCellsOwned, 1); + OcnToCplLayout = + Kokkos::LayoutStride(NExportFields, NCellsOwned, NCellsOwned, 1); + } else { + ABORT_ERROR("SfcCoupling::attachData: Unknown coupling layout"); + } + + CplToOcnView = decltype(CplToOcnView)(CplToOcnData, CplToOcnLayout); + OcnToCplView = decltype(OcnToCplView)(OcnToCplData, OcnToCplLayout); +} + +void SfcCoupling::importFromCoupler() { + + if (CplToOcnView.data() == nullptr) { + ABORT_ERROR( + "CplToOcnView is not attached to data. The SfcCoupling::attachData " + "method must be called before importing data from the coupler."); + } + + // + int TauxIdx = ImportIdx.at("Foxx_taux"); + int TauyIdx = ImportIdx.at("Foxx_tauy"); + + // Copy Kokkos view handles + auto CplToOcnView_ = CplToOcnView; + auto SurfaceStressZonal_ = CplToOcn.SurfaceStressZonal; + auto SurfaceStressMeridional_ = CplToOcn.SurfaceStressMeridional; + + /// TODO: Shouldn't be making direct calls to Kokkos here. + /// How often is threading used? Becuase this will be a serial loop + /// unless threading is used. But this has to be run on the host. + auto Policy = Kokkos::RangePolicy>( + 0, NCellsOwned); + Kokkos::parallel_for("importFromCoupler", Policy, [=](int Idx) { + SurfaceStressZonal_(Idx) = CplToOcnView_(TauxIdx, Idx); + SurfaceStressMeridional_(Idx) = CplToOcnView_(TauyIdx, Idx); + }); +} + CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) : SurfaceStressZonal("SurfaceStressZonal" + Suffix, Mesh->NCellsOwned), SurfaceStressMeridional("SurfaceStressMeridional" + Suffix, diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 896e2453601f..d2e7f9d31272 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -97,7 +97,9 @@ class SfcCoupling { public: std::string Name; - I4 NCellsOwned; ///< Number of cells owned by this task + I4 NCellsOwned; ///< Number of cells owned by this task + I4 NImportFields; ///< Number of fields imported from the coupler + I4 NExportFields; ///< Number of fields exported to the coupler // Coupling Variable containers CplToOcnFields CplToOcn; ///< Coupler to Ocean (x2o) @@ -105,6 +107,16 @@ class SfcCoupling { Alarm CouplingAlarm; ///< Alarm for coupling interval + /// View of Coupler to Ocean (x2o) raw data + Kokkos::View> + CplToOcnView; + + /// View of Ocean to Coupler (o2x) raw data + Kokkos::View> + OcnToCplView; + // Methods /// Create a new surface coupling by calling the constructor and put it @@ -128,9 +140,16 @@ class SfcCoupling { /// Remove surface coupling object by name static void erase(const std::string InName); + /// Get the default surface coupling object static SfcCoupling *getDefault(); + /// Get a surface coupling object by name static SfcCoupling *get(const std::string name); + + /// Create views of the coupling data arrays + void attachData(const Real *CplToOcnData, Real *OcnToCplData); + + void importFromCoupler(); }; } // end namespace OMEGA diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 990905102ed9..3dd3f5a52850 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -20,21 +20,22 @@ using namespace OMEGA; struct TestSetup { - std::map ImportIdx = { - {"Foxx_taux", 0}, {"Foxx_tauy", 1}, {"Foxx_swnet", 2}, - {"Foxx_lwnet", 3}, {"Foxx_lat", 4}, {"Foxx_sen", 5}, - {"Foxx_lwup", 6}, {"Faxa_lwdn", 7}, {"Fioi_melth", 8}, - {"Fioi_bergh", 9}, {"Faxa_snow", 10}, {"Faxa_rain", 11}, - {"Foxx_evap", 12}, {"Fioi_meltw", 13}, {"Fioi_bergw", 14}, - {"Fioi_salt", 15}, {"Foxx_rofl", 16}, {"Foxx_rofi", 17}, - {"Si_ifrac", 18}, {"Si_bpress", 19}, {"Sa_pslv", 20}}; - + std::map ImportIdx = {{"Foxx_taux", 0}, {"Foxx_tauy", 1}}; std::map ExportIdx = { - {"So_t", 0}, {"So_s", 1}, {"So_u", 2}, {"So_v", 3}, - {"So_ssh", 4}, {"So_dhdx", 5}, {"So_dhdy", 6}, {"Fioo_q", 7}, - {"Fioo_frazil", 8}, {"Faoo_h2otemp", 9}}; + {"So_t", 0}, {"So_u", 1}, {"So_v", 2}}; }; +std::string toString(const CouplingLayout &Layout) { + switch (Layout) { + case CouplingLayout::MCT: + return "MCT"; + case CouplingLayout::MOAB: + return "MOAB"; + default: + return "Unknown"; + } +} + int initRawData() { int Err = 0; @@ -90,17 +91,17 @@ int initSfcCouplingTest(const std::string &MeshFile) { return Err; } -int testSfcCoupling() { +int testSfcCoupling(const CouplingLayout Layout, + const TimeInterval CouplingTimeStep) { int Err = 0; TestSetup Setup; - CouplingInitParams CouplingParams; - CouplingParams.ImportIdx = Setup.ImportIdx; - CouplingParams.ExportIdx = Setup.ExportIdx; - CouplingParams.CouplingTimeStep = TimeInterval(3600.0, TimeUnits::Seconds); - CouplingParams.Layout = CouplingLayout::MCT; + CouplingInitParams CouplingParams{.ImportIdx = Setup.ImportIdx, + .ExportIdx = Setup.ExportIdx, + .CouplingTimeStep = CouplingTimeStep, + .Layout = Layout}; Err += SfcCoupling::init(CouplingParams); @@ -115,10 +116,50 @@ int testSfcCoupling() { return -1; } + int NCells = DefCoupling->NCellsOwned; + int NImports = DefCoupling->NImportFields; + int NExports = DefCoupling->NExportFields; + + std::vector CplToOcnData(NCells * NImports, 0.0); + std::vector OcnToCplData(NCells * NExports, 0.0); + + // Index formula depend on the Layout + auto flatIdx = [&](int Cell, int Field) -> int { + if (Layout == CouplingLayout::MCT) + return Cell * NImports + Field; + else // MOAB + return Field * NCells + Cell; + }; + + for (int Cell = 0; Cell < NCells; Cell++) + for (int Field = 0; Field < NImports; Field++) + CplToOcnData[flatIdx(Cell, Field)] = static_cast(Field); + + DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); + DefCoupling->importFromCoupler(); + + bool ImportPass = true; + for (int Cell = 0; Cell < NCells; Cell++) { + if (DefCoupling->CplToOcn.SurfaceStressZonal(Cell) != Real(0)) + ImportPass = false; + if (DefCoupling->CplToOcn.SurfaceStressMeridional(Cell) != Real(1)) + ImportPass = false; + } + + if (ImportPass) { + LOG_INFO("SfcCouplingTest: importFromCoupler with {} layout PASS", + toString(Layout)); + } else { + Err++; + LOG_ERROR("SfcCouplingTest: importFromCoupler with {} layout FAIL", + toString(Layout)); + } + SfcCoupling::clear(); return Err; } + void finalizeSfcCouplingTest() { OceanState::clear(); @@ -140,7 +181,11 @@ int surfaceCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { LOG_CRITICAL("SfcCouplingTest: Error initializing"); } - Err += testSfcCoupling(); + auto *DefTimeStepper = TimeStepper::getDefault(); + TimeInterval OcnTimeStep = DefTimeStepper->getTimeStep(); + + Err += testSfcCoupling(CouplingLayout::MCT, OcnTimeStep); + Err += testSfcCoupling(CouplingLayout::MOAB, OcnTimeStep); if (Err == 0) { LOG_INFO("SfcCouplingTest: Successful completion"); From a3cc9d31f74c0e8a78d0b7ff0f9e39edf0aa6a74 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 26 Jun 2026 07:03:08 -0700 Subject: [PATCH 03/27] Propogate Surface -> Sfc everyhere --- components/omega/src/ocn/SfcCoupling.cpp | 23 +++++++++---------- components/omega/src/ocn/SfcCoupling.h | 20 ++++++++-------- components/omega/test/ocn/SfcCouplingTest.cpp | 14 ++++------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 9edffe3c9353..abd5051c52ee 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -179,9 +179,9 @@ void SfcCoupling::importFromCoupler() { int TauyIdx = ImportIdx.at("Foxx_tauy"); // Copy Kokkos view handles - auto CplToOcnView_ = CplToOcnView; - auto SurfaceStressZonal_ = CplToOcn.SurfaceStressZonal; - auto SurfaceStressMeridional_ = CplToOcn.SurfaceStressMeridional; + auto CplToOcnView_ = CplToOcnView; + auto SfcStressZonal_ = CplToOcn.SfcStressZonal; + auto SfcStressMeridional_ = CplToOcn.SfcStressMeridional; /// TODO: Shouldn't be making direct calls to Kokkos here. /// How often is threading used? Becuase this will be a serial loop @@ -189,19 +189,18 @@ void SfcCoupling::importFromCoupler() { auto Policy = Kokkos::RangePolicy>( 0, NCellsOwned); Kokkos::parallel_for("importFromCoupler", Policy, [=](int Idx) { - SurfaceStressZonal_(Idx) = CplToOcnView_(TauxIdx, Idx); - SurfaceStressMeridional_(Idx) = CplToOcnView_(TauyIdx, Idx); + SfcStressZonal_(Idx) = CplToOcnView_(TauxIdx, Idx); + SfcStressMeridional_(Idx) = CplToOcnView_(TauyIdx, Idx); }); } CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) - : SurfaceStressZonal("SurfaceStressZonal" + Suffix, Mesh->NCellsOwned), - SurfaceStressMeridional("SurfaceStressMeridional" + Suffix, - Mesh->NCellsOwned) {} + : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), + SfcStressMeridional("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) - : SurfaceTemperature("SurfaceTemperature" + Suffix, Mesh->NCellsOwned), - SurfaceVelocityZonal("SurfaceVelocityZonal" + Suffix, Mesh->NCellsOwned), - SurfaceVelocityMeridional("SurfaceVelocityMeridional" + Suffix, - Mesh->NCellsOwned) {} + : SfcTemperature("SfcTemperature" + Suffix, Mesh->NCellsOwned), + SfcVelocityZonal("SfcVelocityZonal" + Suffix, Mesh->NCellsOwned), + SfcVelocityMeridional("SfcVelocityMeridional" + Suffix, + Mesh->NCellsOwned) {} } // namespace OMEGA diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index d2e7f9d31272..9e77556d7e0e 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -1,11 +1,11 @@ #ifndef OMEGA_SURFACECOUPLING_H #define OMEGA_SURFACECOUPLING_H -//===-- ocn/SurfaceCouling.h - surface coupling ----------------*- C++ -*-===// +//===-- ocn/SfcCouling.h - surface coupling ----------------*- C++ -*-===// // /// \file /// \brief Contains the coupling variables exchanged with the coupler /// -/// The SurfaceCouling class contains the variables exchanged with the coupler +/// The SfcCouling class contains the variables exchanged with the coupler /// for a sub-domain of the global horizontal mesh. // //===----------------------------------------------------------------------===// @@ -36,8 +36,8 @@ class CplToOcnFields { // x2o fields only need to be stored on the host. // The SfcCoupling::applyImportFields() method will handle copying the // data to the device. - HostArray1DReal SurfaceStressZonal; ///< Foxx_taux [N m^-2] - HostArray1DReal SurfaceStressMeridional; ///< Foxx_tauy [N m^-2] + HostArray1DReal SfcStressZonal; ///< Foxx_taux [N m^-2] + HostArray1DReal SfcStressMeridional; ///< Foxx_tauy [N m^-2] CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh); }; @@ -46,16 +46,16 @@ class CplToOcnFields { class OcnToCplFields { public: ///< So_t [deg C] - Array1DReal SurfaceTemperature; - HostArray1DReal SurfaceTemperature_H; + Array1DReal SfcTemperature; + HostArray1DReal SfcTemperature_H; ///< So_u [m s^-1] - Array1DReal SurfaceVelocityZonal; - HostArray1DReal SurfaceVelocityZonal_H; + Array1DReal SfcVelocityZonal; + HostArray1DReal SfcVelocityZonal_H; ///< So_v [m s^-1] - Array1DReal SurfaceVelocityMeridional; - HostArray1DReal SurfaceVelocityMeridional_H; + Array1DReal SfcVelocityMeridional; + HostArray1DReal SfcVelocityMeridional_H; OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); }; diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 3dd3f5a52850..0a5151f15d67 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -36,12 +36,6 @@ std::string toString(const CouplingLayout &Layout) { } } -int initRawData() { - int Err = 0; - - return Err; -} - int initSfcCouplingTest(const std::string &MeshFile) { int Err = 0; @@ -140,9 +134,9 @@ int testSfcCoupling(const CouplingLayout Layout, bool ImportPass = true; for (int Cell = 0; Cell < NCells; Cell++) { - if (DefCoupling->CplToOcn.SurfaceStressZonal(Cell) != Real(0)) + if (DefCoupling->CplToOcn.SfcStressZonal(Cell) != Real(0)) ImportPass = false; - if (DefCoupling->CplToOcn.SurfaceStressMeridional(Cell) != Real(1)) + if (DefCoupling->CplToOcn.SfcStressMeridional(Cell) != Real(1)) ImportPass = false; } @@ -173,7 +167,7 @@ void finalizeSfcCouplingTest() { MachEnv::removeAll(); } -int surfaceCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { +int sfcCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { int Err = initSfcCouplingTest(MeshFile); @@ -205,7 +199,7 @@ int main(int argc, char *argv[]) { Pacer::initialize(MPI_COMM_WORLD); Pacer::setPrefix("Omega:"); - RetVal += surfaceCouplingTest(); + RetVal += sfcCouplingTest(); Pacer::finalize(); Kokkos::finalize(); From 036c3936868d9f8d5914bef1a253e921885a8079 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 26 Jun 2026 12:31:18 -0700 Subject: [PATCH 04/27] Add `applyImportFields` method and refactor tests --- components/omega/src/ocn/SfcCoupling.cpp | 11 ++ components/omega/src/ocn/SfcCoupling.h | 15 ++ components/omega/test/ocn/SfcCouplingTest.cpp | 128 +++++++++++++----- 3 files changed, 122 insertions(+), 32 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index abd5051c52ee..ad8298b7b32f 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -194,6 +194,17 @@ void SfcCoupling::importFromCoupler() { }); } +void SfcCoupling::applyImportFields(Forcing *Forcing) { + + // Copy the SfcCoupling host arrays into the Forcing device arrays. + // Copy is only done over the owned cells, since thats all the SfcCoupling + // data is defined over. Forcing will be responsible for halo exchanges. + deepCopy(ownedSubView(Forcing->SfcStressForcing.ZonalStressCell), + CplToOcn.SfcStressZonal); + deepCopy(ownedSubView(Forcing->SfcStressForcing.MeridStressCell), + CplToOcn.SfcStressMeridional); +}; + CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), SfcStressMeridional("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 9e77556d7e0e..82ab33fcd818 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// #include "DataTypes.h" +#include "Forcing.h" #include "HorzMesh.h" #include "TimeMgr.h" #include "TimeStepper.h" @@ -85,6 +86,11 @@ class SfcCoupling { SfcCoupling(const SfcCoupling &) = delete; SfcCoupling(SfcCoupling &&) = delete; + // Create subview that only include the owned cells + template auto ownedSubView(const View &V) const { + return Kokkos::subview(V, std::make_pair(0, NCellsOwned)); + } + // Number of ocn timesteps acccumulated over the coupling interval I4 NAccumSteps; @@ -149,7 +155,16 @@ class SfcCoupling { /// Create views of the coupling data arrays void attachData(const Real *CplToOcnData, Real *OcnToCplData); + /// Import data from the unmanaged view of the coupler data into the + /// SfcCoupling.OcnToCpl object void importFromCoupler(); + + /// Export data from the SfcCoupling.OcnToCpl object into the unmanaged view + /// of the coupler data + // void exportToCoupler(); + + /// Apply the imported data to the Forcing object + void applyImportFields(Forcing *Forcing); }; } // end namespace OMEGA diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 0a5151f15d67..25f894c7ba3d 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -4,6 +4,7 @@ #include "Decomp.h" #include "Eos.h" #include "Field.h" +#include "Forcing.h" #include "Halo.h" #include "HorzMesh.h" #include "IO.h" @@ -25,6 +26,24 @@ struct TestSetup { {"So_t", 0}, {"So_u", 1}, {"So_v", 2}}; }; +CouplingInitParams mockCouplingInitParams( + CouplingLayout Layout = CouplingLayout::MCT, + std::optional CouplingTimeStep = std::nullopt) { + + TestSetup Setup; + + auto *DefTimeStepper = TimeStepper::getDefault(); + TimeInterval CouplingTimeStep_ = + CouplingTimeStep.value_or(DefTimeStepper->getTimeStep()); + + CouplingInitParams CouplingParams{.ImportIdx = Setup.ImportIdx, + .ExportIdx = Setup.ExportIdx, + .CouplingTimeStep = CouplingTimeStep_, + .Layout = Layout}; + + return CouplingParams; +} + std::string toString(const CouplingLayout &Layout) { switch (Layout) { case CouplingLayout::MCT: @@ -81,42 +100,36 @@ int initSfcCouplingTest(const std::string &MeshFile) { } Eos::init(); + Forcing::init(); return Err; } -int testSfcCoupling(const CouplingLayout Layout, - const TimeInterval CouplingTimeStep) { +int testImportFromCoupler(const CouplingLayout Layout) { int Err = 0; - TestSetup Setup; - - CouplingInitParams CouplingParams{.ImportIdx = Setup.ImportIdx, - .ExportIdx = Setup.ExportIdx, - .CouplingTimeStep = CouplingTimeStep, - .Layout = Layout}; - + auto CouplingParams = mockCouplingInitParams(Layout); Err += SfcCoupling::init(CouplingParams); - // test retrival of default SfcCoupling *DefCoupling = SfcCoupling::getDefault(); - if (DefCoupling) { - LOG_INFO("SfcCouplingTest: Default retrival PASS"); - } else { - Err++; - LOG_ERROR("SfcCouplingTest: Default retrival FAIL"); - return -1; - } - int NCells = DefCoupling->NCellsOwned; int NImports = DefCoupling->NImportFields; int NExports = DefCoupling->NExportFields; + int TauxIdx = CouplingParams.ImportIdx.at("Foxx_taux"); + int TauyIdx = CouplingParams.ImportIdx.at("Foxx_tauy"); + std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); + HostArray1DReal ExpectedSfcStressZonal("ExpectedSfcStressZonal", NCells); + HostArray1DReal ExpectedSfcStressMerid("ExpectedSfcStressMerid", NCells); + + deepCopy(ExpectedSfcStressZonal, Real(TauxIdx)); + deepCopy(ExpectedSfcStressMerid, Real(TauyIdx)); + // Index formula depend on the Layout auto flatIdx = [&](int Cell, int Field) -> int { if (Layout == CouplingLayout::MCT) @@ -125,20 +138,18 @@ int testSfcCoupling(const CouplingLayout Layout, return Field * NCells + Cell; }; - for (int Cell = 0; Cell < NCells; Cell++) - for (int Field = 0; Field < NImports; Field++) - CplToOcnData[flatIdx(Cell, Field)] = static_cast(Field); + for (int Cell = 0; Cell < NCells; Cell++) { + CplToOcnData[flatIdx(Cell, TauxIdx)] = static_cast(TauxIdx); + CplToOcnData[flatIdx(Cell, TauyIdx)] = static_cast(TauyIdx); + } DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); DefCoupling->importFromCoupler(); - bool ImportPass = true; - for (int Cell = 0; Cell < NCells; Cell++) { - if (DefCoupling->CplToOcn.SfcStressZonal(Cell) != Real(0)) - ImportPass = false; - if (DefCoupling->CplToOcn.SfcStressMeridional(Cell) != Real(1)) - ImportPass = false; - } + auto ImportPass = arraysEqual(DefCoupling->CplToOcn.SfcStressZonal, + ExpectedSfcStressZonal) && + arraysEqual(DefCoupling->CplToOcn.SfcStressMeridional, + ExpectedSfcStressMerid); if (ImportPass) { LOG_INFO("SfcCouplingTest: importFromCoupler with {} layout PASS", @@ -154,8 +165,62 @@ int testSfcCoupling(const CouplingLayout Layout, return Err; } +int testApplyImportFields() { + + int Err = 0; + + auto CouplingParams = mockCouplingInitParams(); + Err += SfcCoupling::init(CouplingParams); + + Forcing *DefForcing = Forcing::getDefault(); + SfcCoupling *DefCoupling = SfcCoupling::getDefault(); + + int NCells = DefCoupling->NCellsOwned; + int NImports = DefCoupling->NImportFields; + int NExports = DefCoupling->NExportFields; + + int TauxIdx = CouplingParams.ImportIdx.at("Foxx_taux"); + int TauyIdx = CouplingParams.ImportIdx.at("Foxx_tauy"); + + std::vector CplToOcnData(NCells * NImports, 0.0); + std::vector OcnToCplData(NCells * NExports, 0.0); + + HostArray1DReal ExpectedSfcStressZonal("ExpectedSfcStressZonal", NCells); + HostArray1DReal ExpectedSfcStressMerid("ExpectedSfcStressMerid", NCells); + + int Offset = 27; + deepCopy(ExpectedSfcStressZonal, Real(TauxIdx + Offset)); + deepCopy(ExpectedSfcStressMerid, Real(TauyIdx + Offset)); + + // Copy the expected values into the CplToOcn fields directly + deepCopy(DefCoupling->CplToOcn.SfcStressZonal, ExpectedSfcStressZonal); + deepCopy(DefCoupling->CplToOcn.SfcStressMeridional, ExpectedSfcStressMerid); + + DefCoupling->applyImportFields(DefForcing); + + auto SfcStressZonalOwned = Kokkos::subview( + DefForcing->SfcStressForcing.ZonalStressCell, std::pair(0, NCells)); + auto SfcStressMeridOwned = Kokkos::subview( + DefForcing->SfcStressForcing.MeridStressCell, std::pair(0, NCells)); + + auto ApplyPass = arraysEqual(SfcStressZonalOwned, ExpectedSfcStressZonal) && + arraysEqual(SfcStressMeridOwned, ExpectedSfcStressMerid); + + if (ApplyPass) { + LOG_INFO("SfcCouplingTest: applyImportFields PASS"); + } else { + Err++; + LOG_ERROR("SfcCouplingTest: applyImportFields FAIL"); + } + + SfcCoupling::clear(); + + return Err; +} + void finalizeSfcCouplingTest() { + Forcing::clear(); OceanState::clear(); VertCoord::clear(); HorzMesh::clear(); @@ -175,11 +240,10 @@ int sfcCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { LOG_CRITICAL("SfcCouplingTest: Error initializing"); } - auto *DefTimeStepper = TimeStepper::getDefault(); - TimeInterval OcnTimeStep = DefTimeStepper->getTimeStep(); + Err += testImportFromCoupler(CouplingLayout::MCT); + Err += testImportFromCoupler(CouplingLayout::MOAB); - Err += testSfcCoupling(CouplingLayout::MCT, OcnTimeStep); - Err += testSfcCoupling(CouplingLayout::MOAB, OcnTimeStep); + Err += testApplyImportFields(); if (Err == 0) { LOG_INFO("SfcCouplingTest: Successful completion"); From 63d6365f778800df09a04e081c9aa5d900c08d11 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 29 Jun 2026 13:33:45 -0700 Subject: [PATCH 05/27] Rename Meridional to Merid --- components/omega/src/ocn/SfcCoupling.cpp | 17 ++++++++--------- components/omega/src/ocn/SfcCoupling.h | 8 ++++---- components/omega/test/ocn/SfcCouplingTest.cpp | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index ad8298b7b32f..8156258a4977 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -179,9 +179,9 @@ void SfcCoupling::importFromCoupler() { int TauyIdx = ImportIdx.at("Foxx_tauy"); // Copy Kokkos view handles - auto CplToOcnView_ = CplToOcnView; - auto SfcStressZonal_ = CplToOcn.SfcStressZonal; - auto SfcStressMeridional_ = CplToOcn.SfcStressMeridional; + auto CplToOcnView_ = CplToOcnView; + auto SfcStressZonal_ = CplToOcn.SfcStressZonal; + auto SfcStressMerid_ = CplToOcn.SfcStressMerid; /// TODO: Shouldn't be making direct calls to Kokkos here. /// How often is threading used? Becuase this will be a serial loop @@ -189,8 +189,8 @@ void SfcCoupling::importFromCoupler() { auto Policy = Kokkos::RangePolicy>( 0, NCellsOwned); Kokkos::parallel_for("importFromCoupler", Policy, [=](int Idx) { - SfcStressZonal_(Idx) = CplToOcnView_(TauxIdx, Idx); - SfcStressMeridional_(Idx) = CplToOcnView_(TauyIdx, Idx); + SfcStressZonal_(Idx) = CplToOcnView_(TauxIdx, Idx); + SfcStressMerid_(Idx) = CplToOcnView_(TauyIdx, Idx); }); } @@ -202,16 +202,15 @@ void SfcCoupling::applyImportFields(Forcing *Forcing) { deepCopy(ownedSubView(Forcing->SfcStressForcing.ZonalStressCell), CplToOcn.SfcStressZonal); deepCopy(ownedSubView(Forcing->SfcStressForcing.MeridStressCell), - CplToOcn.SfcStressMeridional); + CplToOcn.SfcStressMerid); }; CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), - SfcStressMeridional("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} + SfcStressMerid("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) : SfcTemperature("SfcTemperature" + Suffix, Mesh->NCellsOwned), SfcVelocityZonal("SfcVelocityZonal" + Suffix, Mesh->NCellsOwned), - SfcVelocityMeridional("SfcVelocityMeridional" + Suffix, - Mesh->NCellsOwned) {} + SfcVelocityMerid("SfcVelocityMeridional" + Suffix, Mesh->NCellsOwned) {} } // namespace OMEGA diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 82ab33fcd818..0dced017932b 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -37,8 +37,8 @@ class CplToOcnFields { // x2o fields only need to be stored on the host. // The SfcCoupling::applyImportFields() method will handle copying the // data to the device. - HostArray1DReal SfcStressZonal; ///< Foxx_taux [N m^-2] - HostArray1DReal SfcStressMeridional; ///< Foxx_tauy [N m^-2] + HostArray1DReal SfcStressZonal; ///< Foxx_taux [N m^-2] + HostArray1DReal SfcStressMerid; ///< Foxx_tauy [N m^-2] CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh); }; @@ -55,8 +55,8 @@ class OcnToCplFields { HostArray1DReal SfcVelocityZonal_H; ///< So_v [m s^-1] - Array1DReal SfcVelocityMeridional; - HostArray1DReal SfcVelocityMeridional_H; + Array1DReal SfcVelocityMerid; + HostArray1DReal SfcVelocityMerid_H; OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); }; diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 25f894c7ba3d..3a40daa4f084 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -148,7 +148,7 @@ int testImportFromCoupler(const CouplingLayout Layout) { auto ImportPass = arraysEqual(DefCoupling->CplToOcn.SfcStressZonal, ExpectedSfcStressZonal) && - arraysEqual(DefCoupling->CplToOcn.SfcStressMeridional, + arraysEqual(DefCoupling->CplToOcn.SfcStressMerid, ExpectedSfcStressMerid); if (ImportPass) { @@ -194,7 +194,7 @@ int testApplyImportFields() { // Copy the expected values into the CplToOcn fields directly deepCopy(DefCoupling->CplToOcn.SfcStressZonal, ExpectedSfcStressZonal); - deepCopy(DefCoupling->CplToOcn.SfcStressMeridional, ExpectedSfcStressMerid); + deepCopy(DefCoupling->CplToOcn.SfcStressMerid, ExpectedSfcStressMerid); DefCoupling->applyImportFields(DefForcing); From 8c3ced2f6f1069cc046fb3540333b51585e6fc91 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Wed, 1 Jul 2026 10:58:53 -0700 Subject: [PATCH 06/27] Support cpl pointer array larger than used fields --- components/omega/src/ocn/SfcCoupling.cpp | 34 +++++++++---------- components/omega/src/ocn/SfcCoupling.h | 13 +++++-- components/omega/test/ocn/SfcCouplingTest.cpp | 8 +++-- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 8156258a4977..522956cfc70f 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -38,29 +38,29 @@ int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { } // Create the default surface coupling object and set pointer to it - SfcCoupling::DefaultSfcCoupling = - SfcCoupling::create("Default", DefHorzMesh, CouplingInitParams.ImportIdx, - CouplingInitParams.ExportIdx, DefTimeStepper, - CplTimeStep, CouplingInitParams.Layout); + SfcCoupling::DefaultSfcCoupling = SfcCoupling::create( + "Default", DefHorzMesh, CouplingInitParams.NImportFields, + CouplingInitParams.NImportFields, CouplingInitParams.ImportIdx, + CouplingInitParams.ExportIdx, DefTimeStepper, CplTimeStep, + CouplingInitParams.Layout); return Err; } // Construct a new surface coupling object SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, + const int NImportFields_, const int NExportFields_, const std::map &ImportIdx, const std::map &ExportIdx, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout) - : Name(Name_), ImportIdx(ImportIdx), ExportIdx(ExportIdx), - CplToOcn(Name_, Mesh), OcnToCpl(Name_, Mesh), Layout(Layout) { + : Name(Name_), NImportFields(NImportFields_), NExportFields(NExportFields_), + ImportIdx(ImportIdx), ExportIdx(ExportIdx), CplToOcn(Name_, Mesh), + OcnToCpl(Name_, Mesh), Layout(Layout) { // Retrieve mesh cell count NCellsOwned = Mesh->NCellsOwned; - // Retrieve import/export field counts - NImportFields = ImportIdx.size(); - NExportFields = ExportIdx.size(); NAccumSteps = 0; @@ -79,12 +79,11 @@ SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, // Create a new surface coupling object by calling the constructor and storing // it in the AllSfcCoupling map -SfcCoupling *SfcCoupling::create(const std::string &Name, const HorzMesh *Mesh, - const std::map &ImportIdx, - const std::map &ExportIdx, - TimeStepper *Stepper, - const TimeInterval &CouplingTimeStep, - const CouplingLayout &Layout) { +SfcCoupling *SfcCoupling::create( + const std::string &Name, const HorzMesh *Mesh, const int NImportFields, + const int NExportFields, const std::map &ImportIdx, + const std::map &ExportIdx, TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout) { // Check to see if a surface coupling of the same name already exists if (AllSfcCoupling.find(Name) != AllSfcCoupling.end()) { @@ -96,8 +95,9 @@ SfcCoupling *SfcCoupling::create(const std::string &Name, const HorzMesh *Mesh, // create a new surface coupling on the heap and store it in the map of // unique_ptrs, which will manage its lifetime - auto *NewSfcCoupling = new SfcCoupling(Name, Mesh, ImportIdx, ExportIdx, - Stepper, CouplingTimeStep, Layout); + auto *NewSfcCoupling = + new SfcCoupling(Name, Mesh, NImportFields, NExportFields, ImportIdx, + ExportIdx, Stepper, CouplingTimeStep, Layout); AllSfcCoupling.emplace(Name, NewSfcCoupling); return NewSfcCoupling; diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 0dced017932b..e7e9fa2adc13 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -25,6 +25,8 @@ enum class CouplingLayout { MCT, MOAB }; // Parameters needed to initialize a SfcCoupling object. The information // needed to initialize these parameters is provided by the coupler. struct CouplingInitParams { + int NImportFields; + int NExportFields; std::map ImportIdx; std::map ExportIdx; TimeInterval CouplingTimeStep; @@ -77,6 +79,7 @@ class SfcCoupling { // Construct a new local coupling object SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, + const int NImportFields_, const int NExportFields_, const std::map &ImportIdx, const std::map &ExportIdx, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, @@ -103,9 +106,12 @@ class SfcCoupling { public: std::string Name; - I4 NCellsOwned; ///< Number of cells owned by this task - I4 NImportFields; ///< Number of fields imported from the coupler - I4 NExportFields; ///< Number of fields exported to the coupler + I4 NCellsOwned; ///< Number of cells owned by this task + + // The values below will be larger than InportIdx.size() and ExportIdx.size() + // because omega does not ingest all cpl fields (e.g. BGC, landice), yet... + I4 NImportFields; ///< Num of fields in the x2o pointer array + I4 NExportFields; ///< Num of fields in the o2x pointer array // Coupling Variable containers CplToOcnFields CplToOcn; ///< Coupler to Ocean (x2o) @@ -128,6 +134,7 @@ class SfcCoupling { /// Create a new surface coupling by calling the constructor and put it /// in the AllSfcCoupling map static SfcCoupling *create(const std::string &Name, const HorzMesh *Mesh, + const int NImportFields, const int NExportFields, const std::map &ImportIdx, const std::map &ExportIdx, TimeStepper *Stepper, diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 3a40daa4f084..94688b483d48 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -21,9 +21,9 @@ using namespace OMEGA; struct TestSetup { - std::map ImportIdx = {{"Foxx_taux", 0}, {"Foxx_tauy", 1}}; + std::map ImportIdx = {{"Foxx_taux", 3}, {"Foxx_tauy", 8}}; std::map ExportIdx = { - {"So_t", 0}, {"So_u", 1}, {"So_v", 2}}; + {"So_t", 3}, {"So_u", 6}, {"So_v", 9}}; }; CouplingInitParams mockCouplingInitParams( @@ -36,7 +36,9 @@ CouplingInitParams mockCouplingInitParams( TimeInterval CouplingTimeStep_ = CouplingTimeStep.value_or(DefTimeStepper->getTimeStep()); - CouplingInitParams CouplingParams{.ImportIdx = Setup.ImportIdx, + CouplingInitParams CouplingParams{.NImportFields = 10, + .NExportFields = 10, + .ImportIdx = Setup.ImportIdx, .ExportIdx = Setup.ExportIdx, .CouplingTimeStep = CouplingTimeStep_, .Layout = Layout}; From 2865a74002a3931ab5a99d0ecae6993050a22d71 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 10:19:36 -0700 Subject: [PATCH 07/27] Add getter for private member NAccumSteps --- components/omega/src/ocn/SfcCoupling.cpp | 3 +++ components/omega/src/ocn/SfcCoupling.h | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 522956cfc70f..649f1fc3d02b 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -138,6 +138,9 @@ void SfcCoupling::clear() { DefaultSfcCoupling = nullptr; // prevent dangling pointer } +// Getter for private member NAccumSteps +I4 SfcCoupling::getNAccumSteps() const { return NAccumSteps; } + // Create views of the raw coupling data arrays void SfcCoupling::attachData(const Real *CplToOcnData, Real *OcnToCplData) { diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index e7e9fa2adc13..791b1c965cfa 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -159,6 +159,10 @@ class SfcCoupling { /// Get a surface coupling object by name static SfcCoupling *get(const std::string name); + /// Getter for the number of ocean timestpes accumulated over the coupling + /// interval + I4 getNAccumSteps() const; + /// Create views of the coupling data arrays void attachData(const Real *CplToOcnData, Real *OcnToCplData); From 8ecbd568eb21831a5b606fef23ac6402a415b7b5 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 10:21:34 -0700 Subject: [PATCH 08/27] Add compute method to OcnToCpl fields names Avg - for averaged fields Inst - for instantaneous fields (ie. SSH) Acc - for accumulated fields (to be added later) --- components/omega/src/ocn/SfcCoupling.cpp | 17 ++++++++++++++--- components/omega/src/ocn/SfcCoupling.h | 20 ++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 649f1fc3d02b..fcac3825ebd7 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -213,7 +213,18 @@ CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) SfcStressMerid("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) - : SfcTemperature("SfcTemperature" + Suffix, Mesh->NCellsOwned), - SfcVelocityZonal("SfcVelocityZonal" + Suffix, Mesh->NCellsOwned), - SfcVelocityMerid("SfcVelocityMeridional" + Suffix, Mesh->NCellsOwned) {} + : AvgSfcTemperature("AvgSfcTemperature" + Suffix, Mesh->NCellsOwned), + AvgSfcSalinity("AvgSfcSalinity" + Suffix, Mesh->NCellsOwned), + AvgSfcVelocityZonal("AvgSfcVelocityZonal" + Suffix, Mesh->NCellsOwned), + AvgSfcVelocityMerid("AvgSfcVelocityMeridional" + Suffix, + Mesh->NCellsOwned), + InstSshCellH("InstSshCellH" + Suffix, Mesh->NCellsOwned) { + + // TODO: Init the device values to 0; Doesn't kokkoks do this automatically? + + AvgSfcTemperatureH = createHostMirrorCopy(AvgSfcTemperature); + AvgSfcSalinityH = createHostMirrorCopy(AvgSfcSalinity); + AvgSfcVelocityZonalH = createHostMirrorCopy(AvgSfcVelocityZonal); + AvgSfcVelocityMeridH = createHostMirrorCopy(AvgSfcVelocityMerid); +} } // namespace OMEGA diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 791b1c965cfa..46b925daffe8 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -49,16 +49,24 @@ class CplToOcnFields { class OcnToCplFields { public: ///< So_t [deg C] - Array1DReal SfcTemperature; - HostArray1DReal SfcTemperature_H; + Array1DReal AvgSfcTemperature; + HostArray1DReal AvgSfcTemperatureH; + + ///< So_s [g kg^-1] + Array1DReal AvgSfcSalinity; + HostArray1DReal AvgSfcSalinityH; ///< So_u [m s^-1] - Array1DReal SfcVelocityZonal; - HostArray1DReal SfcVelocityZonal_H; + Array1DReal AvgSfcVelocityZonal; + HostArray1DReal AvgSfcVelocityZonalH; ///< So_v [m s^-1] - Array1DReal SfcVelocityMerid; - HostArray1DReal SfcVelocityMerid_H; + Array1DReal AvgSfcVelocityMerid; + HostArray1DReal AvgSfcVelocityMeridH; + + ///< So_ssh [m] + /// instantaneous field, so no device mirror is needed + HostArray1DReal InstSshCellH; OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); }; From 91411284aee8a9624e250b725c332f578bb03188 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 10:42:47 -0700 Subject: [PATCH 09/27] Add updateExportFields method that runs on device --- components/omega/src/ocn/SfcCoupling.cpp | 49 ++++++++ components/omega/src/ocn/SfcCoupling.h | 12 ++ components/omega/test/ocn/SfcCouplingTest.cpp | 116 +++++++++++++++++- 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index fcac3825ebd7..f106e8d43417 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -1,5 +1,9 @@ #include "SfcCoupling.h" #include "Logging.h" +#include "OceanState.h" +#include "OmegaKokkos.h" +#include "Tracers.h" +#include "VertCoord.h" namespace OMEGA { @@ -208,6 +212,51 @@ void SfcCoupling::applyImportFields(Forcing *Forcing) { CplToOcn.SfcStressMerid); }; +void SfcCoupling::updateExportFields(const OceanState *State, + const Array3DReal &TracerArray) { + + I4 TemperatureIdx, SalinityIdx; + Tracers::getIndex(TemperatureIdx, "Temperature"); + Tracers::getIndex(SalinityIdx, "Salinity"); + + auto Temperature = + Kokkos::subview(TracerArray, TemperatureIdx, Kokkos::ALL, Kokkos::ALL); + auto Salinity = + Kokkos::subview(TracerArray, SalinityIdx, Kokkos::ALL, Kokkos::ALL); + + VertCoord *DefVertCoord = VertCoord::getDefault(); + OMEGA_SCOPE(LocMinLayerCell, DefVertCoord->MinLayerCell); + + OMEGA_SCOPE(LocNAccumSteps, NAccumSteps); + OMEGA_SCOPE(LocAvgSfcSalinity, OcnToCpl.AvgSfcSalinity); + OMEGA_SCOPE(LocAvgSfcTemp, OcnToCpl.AvgSfcTemperature); + OMEGA_SCOPE(LocAvgSfcVelZonal, OcnToCpl.AvgSfcVelocityZonal); + OMEGA_SCOPE(LocAvgSfcVelMerid, OcnToCpl.AvgSfcVelocityMerid); + + // TODO: Implement vector reconsturction for velocity field. + constexpr Real ConstSfcVelocity = 1e-4; + + parallelFor( + {NCellsOwned}, KOKKOS_LAMBDA(int ICell) { + const int KSfc = LocMinLayerCell(ICell); + + // Update the averaged fields using Welford's online algorithm + LocAvgSfcTemp(ICell) = updateAverage( + LocAvgSfcTemp(ICell), Temperature(ICell, KSfc), LocNAccumSteps); + + LocAvgSfcSalinity(ICell) = updateAverage( + LocAvgSfcSalinity(ICell), Salinity(ICell, KSfc), LocNAccumSteps); + + LocAvgSfcVelZonal(ICell) = updateAverage( + LocAvgSfcVelZonal(ICell), ConstSfcVelocity, LocNAccumSteps); + + LocAvgSfcVelMerid(ICell) = updateAverage( + LocAvgSfcVelMerid(ICell), ConstSfcVelocity, LocNAccumSteps); + }); + + NAccumSteps++; +} + CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), SfcStressMerid("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 46b925daffe8..37fc45f84fc5 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -13,6 +13,7 @@ #include "DataTypes.h" #include "Forcing.h" #include "HorzMesh.h" +#include "OceanState.h" #include "TimeMgr.h" #include "TimeStepper.h" @@ -20,6 +21,13 @@ namespace OMEGA { +// Welford's online algorithm for computing a running average +KOKKOS_INLINE_FUNCTION Real updateAverage(const Real OldAvg, + const Real NewValue, + const I4 NAccumSteps) { + return OldAvg + (NewValue - OldAvg) / (NAccumSteps + 1); +} + enum class CouplingLayout { MCT, MOAB }; // Parameters needed to initialize a SfcCoupling object. The information @@ -184,6 +192,10 @@ class SfcCoupling { /// Apply the imported data to the Forcing object void applyImportFields(Forcing *Forcing); + + /// Update the export fields + void updateExportFields(const OceanState *State, + const Array3DReal &TracerArray); }; } // end namespace OMEGA diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 94688b483d48..720eb0cde862 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -23,7 +23,7 @@ struct TestSetup { std::map ImportIdx = {{"Foxx_taux", 3}, {"Foxx_tauy", 8}}; std::map ExportIdx = { - {"So_t", 3}, {"So_u", 6}, {"So_v", 9}}; + {"So_t", 2}, {"So_s", 4}, {"So_u", 6}, {"So_v", 9}}; }; CouplingInitParams mockCouplingInitParams( @@ -103,6 +103,7 @@ int initSfcCouplingTest(const std::string &MeshFile) { Eos::init(); Forcing::init(); + Tracers::init(); return Err; } @@ -220,8 +221,118 @@ int testApplyImportFields() { return Err; } +int testUpdateExportFields(const I4 NSteps) { + + int Err = 0; + + auto *DefStepper = TimeStepper::getDefault(); + Clock *ModelClock = DefStepper->getClock(); + + // Reset the shared clock + ModelClock->setCurrentTime(DefStepper->getStartTime()); + + // Coupling interval spans NSteps ocean timesteps + auto CouplingParams = mockCouplingInitParams( + CouplingLayout::MCT, DefStepper->getTimeStep() * NSteps); + + Err += SfcCoupling::init(CouplingParams); + + SfcCoupling *DefCoupling = SfcCoupling::getDefault(); + OceanState *DefState = OceanState::getDefault(); + VertCoord *DefVertCoord = VertCoord::getDefault(); + + int NCells = DefCoupling->NCellsOwned; + + Real TempBase = static_cast(CouplingParams.ExportIdx.at("So_t")); + Real SalinBase = static_cast(CouplingParams.ExportIdx.at("So_s")); + + I4 TempIdx, SalinIdx; + Tracers::getIndex(TempIdx, "Temperature"); + Tracers::getIndex(SalinIdx, "Salinity"); + + while (!DefCoupling->CouplingAlarm.isRinging()) { + Real CurrStep = static_cast(DefCoupling->getNAccumSteps()); + + HostArray2DReal TempH = Tracers::getHostByIndex(0, TempIdx); + HostArray2DReal SalinH = Tracers::getHostByIndex(0, SalinIdx); + + for (int Cell = 0; Cell < NCells; Cell++) { + int KSfc = DefVertCoord->MinLayerCell(Cell); + + TempH(Cell, KSfc) = TempBase + Cell + CurrStep; + SalinH(Cell, KSfc) = SalinBase + Cell + CurrStep; + } + Tracers::copyToDevice(0); + + DefCoupling->updateExportFields(DefState, Tracers::getAll(0)); + + ModelClock->advance(); + } + + // Sanity check: alarm should ring after NSteps + if (DefCoupling->getNAccumSteps() != NSteps) { + Err++; + LOG_ERROR("SfcCouplingTest: updateExportFields FAIL - " + "NAccumSteps = {}, expected {}", + DefCoupling->getNAccumSteps(), NSteps); + } + + auto AvgTempH = + createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcTemperature); + auto AvgSalinH = createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcSalinity); + + Real StepOffset = static_cast(NSteps - 1) / 2.0; + Real Tol = 1e-10; + + I4 TempErr = 0; + I4 SalinErr = 0; + + // Will this fail for single precision, given the tolerance? + for (int Cell = 0; Cell < NCells; Cell++) { + Real ExpectedTemp = TempBase + Cell + StepOffset; + Real ExpectedSalin = SalinBase + Cell + StepOffset; + + if (std::abs(AvgTempH(Cell) - ExpectedTemp) > Tol) { + TempErr++; + } + + if (std::abs(AvgSalinH(Cell) - ExpectedSalin) > Tol) { + SalinErr++; + } + } + + if (TempErr == 0) { + LOG_INFO("SfcCouplingTest: updateExportFields PASS - " + "AvgSfcTemperature within tolerance of {}", + Tol); + } else { + Err += TempErr; + LOG_ERROR("SfcCouplingTest: updateExportFields FAIL - " + "AvgSfcTemperature outside tolerance of {} for {} cells", + Tol, TempErr); + } + + if (SalinErr == 0) { + LOG_INFO("SfcCouplingTest: updateExportFields PASS - " + "AvgSfcSalinity within tolerance of {}", + Tol); + } else { + Err += SalinErr; + LOG_ERROR("SfcCouplingTest: updateExportFields FAIL - " + "AvgSfcSalinity outside tolerance of {} for {} cells", + Tol, SalinErr); + } + + // reset model clock to the start time for any subsequent tests + ModelClock->setCurrentTime(DefStepper->getStartTime()); + + SfcCoupling::clear(); + return Err; +} + void finalizeSfcCouplingTest() { + Tracers::clear(); Forcing::clear(); OceanState::clear(); VertCoord::clear(); @@ -247,6 +358,9 @@ int sfcCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { Err += testApplyImportFields(); + Err += testUpdateExportFields(1); + Err += testUpdateExportFields(5); + if (Err == 0) { LOG_INFO("SfcCouplingTest: Successful completion"); } From 8e4420db404bb807af0f3c6323abb3d7ca146525 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 11:44:20 -0700 Subject: [PATCH 10/27] Fix bug in OcnToCpl stride for MCT layout --- components/omega/src/ocn/SfcCoupling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index f106e8d43417..df69dccc146d 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -158,7 +158,7 @@ void SfcCoupling::attachData(const Real *CplToOcnData, Real *OcnToCplData) { CplToOcnLayout = Kokkos::LayoutStride(NImportFields, 1, NCellsOwned, NImportFields); OcnToCplLayout = - Kokkos::LayoutStride(NExportFields, 1, NCellsOwned, NImportFields); + Kokkos::LayoutStride(NExportFields, 1, NCellsOwned, NExportFields); } else if (Layout == CouplingLayout::MOAB) { /// MOAB layout: (NImportFields, NCellsOwned) - cell idx strides faster CplToOcnLayout = From cee980d9c03a22e1611411faaf0bde2b80fdac85 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 12:19:30 -0700 Subject: [PATCH 11/27] Make all test use a cell varrying expected value --- components/omega/test/ocn/SfcCouplingTest.cpp | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 720eb0cde862..4dbe8a9b9c25 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -57,6 +57,15 @@ std::string toString(const CouplingLayout &Layout) { } } +HostArray1DReal makeCellVarryingArray(const std::string &Name, const int NCells, + const Real Base) { + HostArray1DReal Array(Name, NCells); + for (int Cell = 0; Cell < NCells; Cell++) { + Array(Cell) = Base + Cell; + } + return Array; +} + int initSfcCouplingTest(const std::string &MeshFile) { int Err = 0; @@ -127,11 +136,10 @@ int testImportFromCoupler(const CouplingLayout Layout) { std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); - HostArray1DReal ExpectedSfcStressZonal("ExpectedSfcStressZonal", NCells); - HostArray1DReal ExpectedSfcStressMerid("ExpectedSfcStressMerid", NCells); - - deepCopy(ExpectedSfcStressZonal, Real(TauxIdx)); - deepCopy(ExpectedSfcStressMerid, Real(TauyIdx)); + HostArray1DReal ExpectedSfcStressZonal = + makeCellVarryingArray("ExpectedSfcStressZonal", NCells, Real(TauxIdx)); + HostArray1DReal ExpectedSfcStressMerid = + makeCellVarryingArray("ExpectedSfcStressMerid", NCells, Real(TauyIdx)); // Index formula depend on the Layout auto flatIdx = [&](int Cell, int Field) -> int { @@ -142,8 +150,8 @@ int testImportFromCoupler(const CouplingLayout Layout) { }; for (int Cell = 0; Cell < NCells; Cell++) { - CplToOcnData[flatIdx(Cell, TauxIdx)] = static_cast(TauxIdx); - CplToOcnData[flatIdx(Cell, TauyIdx)] = static_cast(TauyIdx); + CplToOcnData[flatIdx(Cell, TauxIdx)] = ExpectedSfcStressZonal(Cell); + CplToOcnData[flatIdx(Cell, TauyIdx)] = ExpectedSfcStressMerid(Cell); } DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); @@ -188,12 +196,11 @@ int testApplyImportFields() { std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); - HostArray1DReal ExpectedSfcStressZonal("ExpectedSfcStressZonal", NCells); - HostArray1DReal ExpectedSfcStressMerid("ExpectedSfcStressMerid", NCells); - - int Offset = 27; - deepCopy(ExpectedSfcStressZonal, Real(TauxIdx + Offset)); - deepCopy(ExpectedSfcStressMerid, Real(TauyIdx + Offset)); + int Offset = 27; + HostArray1DReal ExpectedSfcStressZonal = makeCellVarryingArray( + "ExpectedSfcStressZonal", NCells, Real(TauxIdx + Offset)); + HostArray1DReal ExpectedSfcStressMerid = makeCellVarryingArray( + "ExpectedSfcStressMerid", NCells, Real(TauyIdx + Offset)); // Copy the expected values into the CplToOcn fields directly deepCopy(DefCoupling->CplToOcn.SfcStressZonal, ExpectedSfcStressZonal); @@ -281,22 +288,23 @@ int testUpdateExportFields(const I4 NSteps) { createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcTemperature); auto AvgSalinH = createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcSalinity); - Real StepOffset = static_cast(NSteps - 1) / 2.0; Real Tol = 1e-10; + Real StepOffset = static_cast(NSteps - 1) / 2.0; + HostArray1DReal ExpectedTemp = + makeCellVarryingArray("ExpectedTemp", NCells, TempBase + StepOffset); + HostArray1DReal ExpectedSalin = + makeCellVarryingArray("ExpectedSalin", NCells, SalinBase + StepOffset); I4 TempErr = 0; I4 SalinErr = 0; // Will this fail for single precision, given the tolerance? for (int Cell = 0; Cell < NCells; Cell++) { - Real ExpectedTemp = TempBase + Cell + StepOffset; - Real ExpectedSalin = SalinBase + Cell + StepOffset; - - if (std::abs(AvgTempH(Cell) - ExpectedTemp) > Tol) { + if (std::abs(AvgTempH(Cell) - ExpectedTemp(Cell)) > Tol) { TempErr++; } - if (std::abs(AvgSalinH(Cell) - ExpectedSalin) > Tol) { + if (std::abs(AvgSalinH(Cell) - ExpectedSalin(Cell)) > Tol) { SalinErr++; } } From ef68693e4a27a508bc8e1a61e7e2d32dc1547bf3 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 12:37:04 -0700 Subject: [PATCH 12/27] Make flatIdx function work for both import/export --- components/omega/test/ocn/SfcCouplingTest.cpp | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 4dbe8a9b9c25..47aae2c8b074 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -66,6 +66,15 @@ HostArray1DReal makeCellVarryingArray(const std::string &Name, const int NCells, return Array; } +// Shaed index formula for packing/unpacking from rae coupler buffer +int flatIdx(const CouplingLayout Layout, const int Cell, const int Field, + const int NCells, const int NFields) { + if (Layout == CouplingLayout::MCT) + return Cell * NFields + Field; + else // MOAB + return Field * NCells + Cell; +} + int initSfcCouplingTest(const std::string &MeshFile) { int Err = 0; @@ -141,17 +150,11 @@ int testImportFromCoupler(const CouplingLayout Layout) { HostArray1DReal ExpectedSfcStressMerid = makeCellVarryingArray("ExpectedSfcStressMerid", NCells, Real(TauyIdx)); - // Index formula depend on the Layout - auto flatIdx = [&](int Cell, int Field) -> int { - if (Layout == CouplingLayout::MCT) - return Cell * NImports + Field; - else // MOAB - return Field * NCells + Cell; - }; - for (int Cell = 0; Cell < NCells; Cell++) { - CplToOcnData[flatIdx(Cell, TauxIdx)] = ExpectedSfcStressZonal(Cell); - CplToOcnData[flatIdx(Cell, TauyIdx)] = ExpectedSfcStressMerid(Cell); + CplToOcnData[flatIdx(Layout, Cell, TauxIdx, NCells, NImports)] = + ExpectedSfcStressZonal(Cell); + CplToOcnData[flatIdx(Layout, Cell, TauyIdx, NCells, NImports)] = + ExpectedSfcStressMerid(Cell); } DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); From df98ebb51ffc5032b57a0245fea222033302fc5d Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 13:41:22 -0700 Subject: [PATCH 13/27] Add copyToHost and resetField methods to OcnToCpl --- components/omega/src/ocn/SfcCoupling.cpp | 30 +++++++++++++++++++++++- components/omega/src/ocn/SfcCoupling.h | 6 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index df69dccc146d..2029f4418b7f 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -269,11 +269,39 @@ OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) Mesh->NCellsOwned), InstSshCellH("InstSshCellH" + Suffix, Mesh->NCellsOwned) { - // TODO: Init the device values to 0; Doesn't kokkoks do this automatically? + // Kokkok views created with a label are zero-initialized by default. + // We reset the feilds here anyway to be explicit about the fact that the + // OcnToCpl fields need to being a coupling interval with all zeros. + resetFields(); AvgSfcTemperatureH = createHostMirrorCopy(AvgSfcTemperature); AvgSfcSalinityH = createHostMirrorCopy(AvgSfcSalinity); AvgSfcVelocityZonalH = createHostMirrorCopy(AvgSfcVelocityZonal); AvgSfcVelocityMeridH = createHostMirrorCopy(AvgSfcVelocityMerid); } + +void OcnToCplFields::copyToHost() { + + deepCopy(AvgSfcTemperatureH, AvgSfcTemperature); + deepCopy(AvgSfcSalinityH, AvgSfcSalinity); + deepCopy(AvgSfcVelocityZonalH, AvgSfcVelocityZonal); + deepCopy(AvgSfcVelocityMeridH, AvgSfcVelocityMerid); + + // SSH is an instantaneous field, so we don't bother with a device mirror of + // our own. Instead, copy from the VertCoord, which owns SSH, host array. + VertCoord *DefVertCoord = VertCoord::getDefault(); + + auto SSHCellOwned = Kokkos::subview( + DefVertCoord->SshCell, std::make_pair(0, (int)InstSshCellH.extent(0))); + + deepCopy(InstSshCellH, SSHCellOwned); +} + +// OcnToCpl fields need to being a coupling interval with all values set to 0. +void OcnToCplFields::resetFields() { + deepCopy(AvgSfcTemperature, 0.0); + deepCopy(AvgSfcSalinity, 0.0); + deepCopy(AvgSfcVelocityZonal, 0.0); + deepCopy(AvgSfcVelocityMerid, 0.0); +} } // namespace OMEGA diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 37fc45f84fc5..ef324b1cc1b9 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -76,6 +76,12 @@ class OcnToCplFields { /// instantaneous field, so no device mirror is needed HostArray1DReal InstSshCellH; + // Refresh host mirrors from their devide counterparts + void copyToHost(); + + // Reset all fields to 0 + void resetFields(); + OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); }; From fd24a41e81390ba572cc48f4fb039c1bfdf29834 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 13:43:45 -0700 Subject: [PATCH 14/27] Add exportToCoupler method --- components/omega/src/ocn/SfcCoupling.cpp | 39 ++++++ components/omega/src/ocn/SfcCoupling.h | 8 +- components/omega/test/ocn/SfcCouplingTest.cpp | 124 +++++++++++++++++- 3 files changed, 165 insertions(+), 6 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 2029f4418b7f..999ed444c87e 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -201,6 +201,45 @@ void SfcCoupling::importFromCoupler() { }); } +void SfcCoupling::exportToCoupler() { + + if (OcnToCplView.data() == nullptr) { + ABORT_ERROR( + "OcnToCplView is not attached to data. The SfcCoupling::attachData " + "method must be called before exporting data to the coupler."); + } + + // Copy the OcnToCpl fields to their host mirrors + OcnToCpl.copyToHost(); + + int TempIdx = ExportIdx.at("So_t"); + int SalinIdx = ExportIdx.at("So_s"); + int VelUIdx = ExportIdx.at("So_u"); + int VelVIdx = ExportIdx.at("So_v"); + int SshIdx = ExportIdx.at("So_ssh"); + + // Copy Kokkos view handles + auto OcnToCplView_ = OcnToCplView; + auto AvgSfcTemperature_ = OcnToCpl.AvgSfcTemperatureH; + auto AvgSfcSalinity_ = OcnToCpl.AvgSfcSalinityH; + auto AvgSfcVelocityZonal_ = OcnToCpl.AvgSfcVelocityZonalH; + auto AvgSfcVelocityMerid_ = OcnToCpl.AvgSfcVelocityMeridH; + auto InstSshCellH_ = OcnToCpl.InstSshCellH; + + /// TODO: Shouldn't be making direct calls to Kokkos here. + auto Policy = Kokkos::RangePolicy>( + 0, NCellsOwned); + Kokkos::parallel_for("exportToCoupler", Policy, [=](int Idx) { + OcnToCplView_(TempIdx, Idx) = AvgSfcTemperature_(Idx); + OcnToCplView_(SalinIdx, Idx) = AvgSfcSalinity_(Idx); + OcnToCplView_(VelUIdx, Idx) = AvgSfcVelocityZonal_(Idx); + OcnToCplView_(VelVIdx, Idx) = AvgSfcVelocityMerid_(Idx); + OcnToCplView_(SshIdx, Idx) = InstSshCellH_(Idx); + }); + + OcnToCpl.resetFields(); // Reset fields to 0 for the next coupling interval + NAccumSteps = 0; // Reset step counter for the next coupling interval +} void SfcCoupling::applyImportFields(Forcing *Forcing) { // Copy the SfcCoupling host arrays into the Forcing device arrays. diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index ef324b1cc1b9..2d123be717c2 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -188,13 +188,11 @@ class SfcCoupling { /// Create views of the coupling data arrays void attachData(const Real *CplToOcnData, Real *OcnToCplData); - /// Import data from the unmanaged view of the coupler data into the - /// SfcCoupling.OcnToCpl object + /// Import data from the unmanaged view of x2o pointer into OcnToCpl object void importFromCoupler(); - /// Export data from the SfcCoupling.OcnToCpl object into the unmanaged view - /// of the coupler data - // void exportToCoupler(); + /// Export data from OcnToCpl object into the unmanaged view of o2x pointer + void exportToCoupler(); /// Apply the imported data to the Forcing object void applyImportFields(Forcing *Forcing); diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 47aae2c8b074..2433d9004b03 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -23,7 +23,7 @@ struct TestSetup { std::map ImportIdx = {{"Foxx_taux", 3}, {"Foxx_tauy", 8}}; std::map ExportIdx = { - {"So_t", 2}, {"So_s", 4}, {"So_u", 6}, {"So_v", 9}}; + {"So_t", 2}, {"So_s", 4}, {"So_u", 6}, {"So_v", 9}, {"So_ssh", 1}}; }; CouplingInitParams mockCouplingInitParams( @@ -341,6 +341,125 @@ int testUpdateExportFields(const I4 NSteps) { return Err; } +int testExportToCoupler(const CouplingLayout Layout) { + + int Err = 0; + + auto CouplingParams = mockCouplingInitParams(Layout); + Err += SfcCoupling::init(CouplingParams); + + SfcCoupling *DefCoupling = SfcCoupling::getDefault(); + VertCoord *DefVertCoord = VertCoord::getDefault(); + + int NCells = DefCoupling->NCellsOwned; + int NImports = DefCoupling->NImportFields; + int NExports = DefCoupling->NExportFields; + + std::vector CplToOcnData(NCells * NImports, 0.0); + std::vector OcnToCplData(NCells * NExports, 0.0); + + int TempIdx = CouplingParams.ExportIdx.at("So_t"); + int SalinIdx = CouplingParams.ExportIdx.at("So_s"); + int VelUIdx = CouplingParams.ExportIdx.at("So_u"); + int VelVIdx = CouplingParams.ExportIdx.at("So_v"); + int SshIdx = CouplingParams.ExportIdx.at("So_ssh"); + + DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); + + HostArray1DReal ExpectedTemp = + makeCellVarryingArray("ExpectedTemp", NCells, Real(TempIdx)); + HostArray1DReal ExpectedSalin = + makeCellVarryingArray("ExpectedSalin", NCells, Real(SalinIdx)); + HostArray1DReal ExpectedVelU = + makeCellVarryingArray("ExpectedVelU", NCells, Real(VelUIdx)); + HostArray1DReal ExpectedVelV = + makeCellVarryingArray("ExpectedVelV", NCells, Real(VelVIdx)); + HostArray1DReal ExpectedSsh = + makeCellVarryingArray("ExpectedSsh", NCells, Real(SshIdx)); + + deepCopy(DefCoupling->OcnToCpl.AvgSfcTemperature, ExpectedTemp); + deepCopy(DefCoupling->OcnToCpl.AvgSfcSalinity, ExpectedSalin); + deepCopy(DefCoupling->OcnToCpl.AvgSfcVelocityZonal, ExpectedVelU); + deepCopy(DefCoupling->OcnToCpl.AvgSfcVelocityMerid, ExpectedVelV); + + auto SshCellOwned = + Kokkos::subview(DefVertCoord->SshCell, std::pair(0, NCells)); + deepCopy(SshCellOwned, ExpectedSsh); + + DefCoupling->exportToCoupler(); + + // Check 1: exportToCoupler properly packs into OcnToCplView + int PackErr = 0; + for (int Cell = 0; Cell < NCells; Cell++) { + if (OcnToCplData[flatIdx(Layout, Cell, TempIdx, NCells, NExports)] != + ExpectedTemp(Cell)) { + PackErr++; + } + if (OcnToCplData[flatIdx(Layout, Cell, SalinIdx, NCells, NExports)] != + ExpectedSalin(Cell)) { + PackErr++; + } + if (OcnToCplData[flatIdx(Layout, Cell, VelUIdx, NCells, NExports)] != + ExpectedVelU(Cell)) { + PackErr++; + } + if (OcnToCplData[flatIdx(Layout, Cell, VelVIdx, NCells, NExports)] != + ExpectedVelV(Cell)) { + PackErr++; + } + if (OcnToCplData[flatIdx(Layout, Cell, SshIdx, NCells, NExports)] != + ExpectedSsh(Cell)) { + PackErr++; + } + } + + if (PackErr == 0) { + LOG_INFO("SfcCouplingTest: exportToCoupler with {} layout PASS", + toString(Layout)); + } else { + Err += PackErr; + LOG_ERROR("SfcCouplingTest: exportToCoupler with {} layout FAIL - " + "{} packing errors", + toString(Layout), PackErr); + } + + // Check 2: resetFields() zeroed the running-average accumulators + HostArray1DReal ZeroArray("ZeroArray", NCells); + deepCopy(ZeroArray, 0.0); + + // Refresh OcnToCpl's own host mirrors post-reset, rather than creating + // separate test-only mirrors + DefCoupling->OcnToCpl.copyToHost(); + + int ResetErr = 0; + if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcTemperatureH, ZeroArray)) + ResetErr++; + if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcSalinityH, ZeroArray)) + ResetErr++; + if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcVelocityZonalH, ZeroArray)) + ResetErr++; + if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcVelocityMeridH, ZeroArray)) + ResetErr++; + + if (ResetErr == 0) { + LOG_INFO("SfcCouplingTest: exportToCoupler resetFields PASS"); + } else { + Err += ResetErr; + LOG_ERROR("SfcCouplingTest: exportToCoupler resetFields FAIL"); + } + + // Check 3: NAccumSteps was reset to 0 + if (DefCoupling->getNAccumSteps() != 0) { + Err++; + LOG_ERROR("SfcCouplingTest: exportToCoupler reset counter FAIL"); + } else { + LOG_INFO("SfcCouplingTest: exportToCoupler reset counter PASS"); + } + + SfcCoupling::clear(); + + return Err; +} void finalizeSfcCouplingTest() { Tracers::clear(); @@ -372,6 +491,9 @@ int sfcCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { Err += testUpdateExportFields(1); Err += testUpdateExportFields(5); + Err += testExportToCoupler(CouplingLayout::MCT); + Err += testExportToCoupler(CouplingLayout::MOAB); + if (Err == 0) { LOG_INFO("SfcCouplingTest: Successful completion"); } From 9c86c654b0bfc996dc9500c342586c4d47e683b2 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 13:54:11 -0700 Subject: [PATCH 15/27] Used `isApprox` for comparison --- components/omega/test/ocn/SfcCouplingTest.cpp | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 2433d9004b03..c72dd5e39516 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -11,6 +11,7 @@ #include "IOStream.h" #include "Logging.h" #include "MachEnv.h" +#include "OceanTestCommon.h" #include "OmegaKokkos.h" #include "Pacer.h" #include "TimeStepper.h" @@ -291,7 +292,10 @@ int testUpdateExportFields(const I4 NSteps) { createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcTemperature); auto AvgSalinH = createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcSalinity); - Real Tol = 1e-10; + // Only accumulated round-off error (no discretization/truncation error), + // so use a tighter tolerance than tests comparing against analytic + // solutions of numerical operators. + Real RTol = sizeof(Real) == 4 ? 1e-5 : 1e-10; Real StepOffset = static_cast(NSteps - 1) / 2.0; HostArray1DReal ExpectedTemp = makeCellVarryingArray("ExpectedTemp", NCells, TempBase + StepOffset); @@ -301,37 +305,38 @@ int testUpdateExportFields(const I4 NSteps) { I4 TempErr = 0; I4 SalinErr = 0; - // Will this fail for single precision, given the tolerance? for (int Cell = 0; Cell < NCells; Cell++) { - if (std::abs(AvgTempH(Cell) - ExpectedTemp(Cell)) > Tol) { + if (!isApprox(AvgTempH(Cell), ExpectedTemp(Cell), RTol)) { TempErr++; } - if (std::abs(AvgSalinH(Cell) - ExpectedSalin(Cell)) > Tol) { + if (!isApprox(AvgSalinH(Cell), ExpectedSalin(Cell), RTol)) { SalinErr++; } } if (TempErr == 0) { LOG_INFO("SfcCouplingTest: updateExportFields PASS - " - "AvgSfcTemperature within tolerance of {}", - Tol); + "AvgSfcTemperature within relative tolerance of {}", + RTol); } else { Err += TempErr; LOG_ERROR("SfcCouplingTest: updateExportFields FAIL - " - "AvgSfcTemperature outside tolerance of {} for {} cells", - Tol, TempErr); + "AvgSfcTemperature outside relative tolerance of {} for {} " + "cells", + RTol, TempErr); } if (SalinErr == 0) { LOG_INFO("SfcCouplingTest: updateExportFields PASS - " - "AvgSfcSalinity within tolerance of {}", - Tol); + "AvgSfcSalinity within relative tolerance of {}", + RTol); } else { Err += SalinErr; LOG_ERROR("SfcCouplingTest: updateExportFields FAIL - " - "AvgSfcSalinity outside tolerance of {} for {} cells", - Tol, SalinErr); + "AvgSfcSalinity outside relative tolerance of {} for {} " + "cells", + RTol, SalinErr); } // reset model clock to the start time for any subsequent tests From b6068e097b9f1d245057c40fde81d27880ebddd8 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 14:13:03 -0700 Subject: [PATCH 16/27] Add first pass at documentation --- components/omega/doc/devGuide/SfcCoupling.md | 71 +++++++++++++++++++ components/omega/doc/index.md | 2 + components/omega/doc/userGuide/SfcCoupling.md | 7 ++ 3 files changed, 80 insertions(+) create mode 100644 components/omega/doc/devGuide/SfcCoupling.md create mode 100644 components/omega/doc/userGuide/SfcCoupling.md diff --git a/components/omega/doc/devGuide/SfcCoupling.md b/components/omega/doc/devGuide/SfcCoupling.md new file mode 100644 index 000000000000..e1646ce6028c --- /dev/null +++ b/components/omega/doc/devGuide/SfcCoupling.md @@ -0,0 +1,71 @@ +(omega-dev-sfc-coupling)= + +# Surface Coupling + +The `SfcCoupling` class manages the variables exchanged to (`o2x`) and from +(`x2o`) the coupler. It handles import/export of the raw coupler data, +application of imported fields to the [`Forcing`](#omega-dev-forcing) object, +and accumulation of export fields over the coupling interval. It is possible +to have multiple `SfcCoupling` instances; every instance has a name and is +tracked in a static C++ map called `AllSfcCoupling`. + +## Initialization + +The static method: +```c++ +OMEGA::SfcCoupling::init(CouplingInitParams); +``` +initializes the default `SfcCoupling` given a `CouplingInitParams` struct +(number of import/export fields, name-to-index maps, coupling time step, and +coupling `Layout`, i.e. `MCT` or `MOAB`) supplied by the coupler. A pointer to +the default instance can be retrieved at any time using: +```c++ +OMEGA::SfcCoupling* DefSfcCoupling = OMEGA::SfcCoupling::getDefault(); +``` + +## Creation of non-default surface coupling objects + +A non-default instance can be created with the static `create` method, +which returns a pointer to the newly created object: +```c++ +OMEGA::SfcCoupling* NewSfcCoupling = OMEGA::SfcCoupling::create( + Name, Mesh, NImportFields, NExportFields, ImportIdx, ExportIdx, + Stepper, CouplingTimeStep, Layout); +``` +Given its name, a pointer to a named instance can be obtained at any time: +```c++ +OMEGA::SfcCoupling* NewSfcCoupling = OMEGA::SfcCoupling::get(Name); +``` + +## Data exchange + +Before import/export, unmanaged views must be attached to the raw coupler +data pointers: +```c++ +SfcCoupling.attachData(CplToOcnData, OcnToCplData); +``` +To copy data from the coupler into the `CplToOcn` fields, and from the +`OcnToCpl` fields back out to the coupler: +```c++ +SfcCoupling.importFromCoupler(); +SfcCoupling.exportToCoupler(); +``` +Imported fields are applied to a `Forcing` object with: +```c++ +SfcCoupling.applyImportFields(ForcingPtr); +``` +Export fields are accumulated (running average) each ocean time step with: +```c++ +SfcCoupling.updateExportFields(State, TracerArray); +``` + +## Removal of surface coupling objects + +To erase a specific named instance use `erase`: +```c++ +OMEGA::SfcCoupling::erase(Name); +``` +To clear all instances do: +```c++ +OMEGA::SfcCoupling::clear(); +``` diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md index 8d1b38501df8..aee6135d35a2 100644 --- a/components/omega/doc/index.md +++ b/components/omega/doc/index.md @@ -54,6 +54,7 @@ userGuide/Timing userGuide/VerticalMixingCoeff userGuide/VertAdv userGuide/Forcing +userGuide/SfcCoupling ``` ```{toctree} @@ -100,6 +101,7 @@ devGuide/Timing devGuide/VerticalMixingCoeff devGuide/VertAdv devGuide/Forcing +devGuide/SfcCoupling ``` ```{toctree} diff --git a/components/omega/doc/userGuide/SfcCoupling.md b/components/omega/doc/userGuide/SfcCoupling.md new file mode 100644 index 000000000000..acb1f3d7d624 --- /dev/null +++ b/components/omega/doc/userGuide/SfcCoupling.md @@ -0,0 +1,7 @@ +(omega-user-sfc-coupling)= + +## Surface Coupling + +The `SfcCoupling` class manages the exchange of surface variables with the +coupler (MCT or MOAB) between Omega and the other components of E3SM. There are +no user-configurable options. From 75f4903ec35824a91f81f0d7631d7bf2b19909cd Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 14:20:59 -0700 Subject: [PATCH 17/27] Fix up comments --- components/omega/src/ocn/SfcCoupling.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 999ed444c87e..df64e1481bb8 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -11,7 +11,8 @@ namespace OMEGA { SfcCoupling *SfcCoupling::DefaultSfcCoupling = nullptr; std::map> SfcCoupling::AllSfcCoupling; -// Initalize the surface coupling. Assumes the ... have been initialized +// Initalize the surface coupling. Assumes the default HorzMesh and +// TimeStepper have been initialized int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { int Err = 0; // default successful return code @@ -73,7 +74,7 @@ SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, Clock *StepperClock = Stepper->getClock(); TimeInstant StartTime = Stepper->getStartTime(); - // Create a CouplingAlarm associated with CouplingTimeStep + // Avoid alarm name collisions on the shared clock for non-default instances if (Name_ != "Default") AlarmName += Name_; @@ -181,7 +182,7 @@ void SfcCoupling::importFromCoupler() { "method must be called before importing data from the coupler."); } - // + // Get import field indices for surface stress components int TauxIdx = ImportIdx.at("Foxx_taux"); int TauyIdx = ImportIdx.at("Foxx_tauy"); From a60dd3119f9d5348ff00b15a59db99b1c7e9f9ba Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 14:23:37 -0700 Subject: [PATCH 18/27] Fix bug where NImportFields was passed twice --- components/omega/src/ocn/SfcCoupling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index df64e1481bb8..21b1d728ec09 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -45,7 +45,7 @@ int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { // Create the default surface coupling object and set pointer to it SfcCoupling::DefaultSfcCoupling = SfcCoupling::create( "Default", DefHorzMesh, CouplingInitParams.NImportFields, - CouplingInitParams.NImportFields, CouplingInitParams.ImportIdx, + CouplingInitParams.NExportFields, CouplingInitParams.ImportIdx, CouplingInitParams.ExportIdx, DefTimeStepper, CplTimeStep, CouplingInitParams.Layout); From 50bb8ba75c21e8159b8ab0901d3ba7377e9bace4 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 15:03:09 -0700 Subject: [PATCH 19/27] Add test for non-default name and erasing --- components/omega/test/ocn/SfcCouplingTest.cpp | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index c72dd5e39516..c2a27155a2a3 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -465,6 +465,43 @@ int testExportToCoupler(const CouplingLayout Layout) { return Err; } + +int testEraseAndGet() { + + int Err = 0; + + TestSetup Setup; + auto *DefMesh = HorzMesh::getDefault(); + auto *DefStepper = TimeStepper::getDefault(); + TimeInterval TimeStep = DefStepper->getTimeStep(); + + // test creation of a non-default, named surface coupling object + SfcCoupling::create("AnotherSfcCoupling", DefMesh, 10, 12, Setup.ImportIdx, + Setup.ExportIdx, DefStepper, TimeStep, + CouplingLayout::MCT); + + if (SfcCoupling::get("AnotherSfcCoupling")) { + LOG_INFO("SfcCouplingTest: Non-default SfcCoupling retrieval PASS"); + } else { + Err++; + LOG_ERROR("SfcCouplingTest: Non-default SfcCoupling retrieval FAIL"); + } + + // test erase + SfcCoupling::erase("AnotherSfcCoupling"); + + // get() logs an expected error here; wrap with spdlog::set_level to + // silence it if the message becomes confusing in test logs + if (SfcCoupling::get("AnotherSfcCoupling")) { + Err++; + LOG_ERROR("SfcCouplingTest: erased SfcCoupling retrieval FAIL"); + } else { + LOG_INFO("SfcCouplingTest: erased SfcCoupling retrieval PASS"); + } + + return Err; +} + void finalizeSfcCouplingTest() { Tracers::clear(); @@ -499,6 +536,8 @@ int sfcCouplingTest(const std::string &MeshFile = "OmegaMesh.nc") { Err += testExportToCoupler(CouplingLayout::MCT); Err += testExportToCoupler(CouplingLayout::MOAB); + Err += testEraseAndGet(); + if (Err == 0) { LOG_INFO("SfcCouplingTest: Successful completion"); } From 39f8e495fd212d4a3f77eafb194f14ecee3ff8d8 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 3 Jul 2026 10:49:43 -0700 Subject: [PATCH 20/27] Temp. conversion once per cpl interval on device Host array for the OcnToCplFields class were made private to protect from different units on host and device array. Needed to balance performance with correct units. --- components/omega/doc/devGuide/SfcCoupling.md | 12 ++ components/omega/src/ocn/SfcCoupling.cpp | 109 ++++++++++++------ components/omega/src/ocn/SfcCoupling.h | 33 ++++-- components/omega/test/ocn/SfcCouplingTest.cpp | 75 +++++++----- 4 files changed, 155 insertions(+), 74 deletions(-) diff --git a/components/omega/doc/devGuide/SfcCoupling.md b/components/omega/doc/devGuide/SfcCoupling.md index e1646ce6028c..7ea848c74fde 100644 --- a/components/omega/doc/devGuide/SfcCoupling.md +++ b/components/omega/doc/devGuide/SfcCoupling.md @@ -59,6 +59,18 @@ Export fields are accumulated (running average) each ocean time step with: SfcCoupling.updateExportFields(State, TracerArray); ``` +## Units + +`OcnToCplFields`'s averaged temperature is Conservative Temperature (deg C) +on device (`AvgSfcTemperature`), invariant at all times. `copyToHost()` +converts it to in-situ temperature in Kelvin (via TEOS-10, when in use) +into the host mirror `AvgSfcTemperatureH`, once per coupling interval. +All other averaged fields keep the same units and value on host and device. + +To keep this invariant, `OcnToCplFields`'s averaged device arrays are +private: they can only be written via `updateAverages()` (called each +ocean time step from `updateExportFields()`) and read via `copyToHost()`. + ## Removal of surface coupling objects To erase a specific named instance use `erase`: diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 21b1d728ec09..fcc0bf6398b7 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -1,4 +1,6 @@ #include "SfcCoupling.h" +#include "Eos.h" +#include "GlobalConstants.h" #include "Logging.h" #include "OceanState.h" #include "OmegaKokkos.h" @@ -255,6 +257,40 @@ void SfcCoupling::applyImportFields(Forcing *Forcing) { void SfcCoupling::updateExportFields(const OceanState *State, const Array3DReal &TracerArray) { + OcnToCpl.updateAverages(State, TracerArray, NAccumSteps, NCellsOwned); + + NAccumSteps++; +} + +CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) + : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), + SfcStressMerid("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} + +OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) + : AvgSfcTemperature("AvgSfcTemperature" + Suffix, Mesh->NCellsOwned), + AvgSfcSalinity("AvgSfcSalinity" + Suffix, Mesh->NCellsOwned), + AvgSfcVelocityZonal("AvgSfcVelocityZonal" + Suffix, Mesh->NCellsOwned), + AvgSfcVelocityMerid("AvgSfcVelocityMeridional" + Suffix, + Mesh->NCellsOwned), + InstSshCellH("InstSshCellH" + Suffix, Mesh->NCellsOwned), + InSituTempScratch("InSituTempScratch" + Suffix, Mesh->NCellsOwned) { + + // Kokkok views created with a label are zero-initialized by default. + // We reset the feilds here anyway to be explicit about the fact that the + // OcnToCpl fields need to being a coupling interval with all zeros. + resetFields(); + + AvgSfcTemperatureH = createHostMirrorCopy(AvgSfcTemperature); + AvgSfcSalinityH = createHostMirrorCopy(AvgSfcSalinity); + AvgSfcVelocityZonalH = createHostMirrorCopy(AvgSfcVelocityZonal); + AvgSfcVelocityMeridH = createHostMirrorCopy(AvgSfcVelocityMerid); +} + +void OcnToCplFields::updateAverages(const OceanState *State, + const Array3DReal &TracerArray, + const I4 NAccumSteps, + const I4 NCellsOwned) { + I4 TemperatureIdx, SalinityIdx; Tracers::getIndex(TemperatureIdx, "Temperature"); Tracers::getIndex(SalinityIdx, "Salinity"); @@ -265,13 +301,12 @@ void SfcCoupling::updateExportFields(const OceanState *State, Kokkos::subview(TracerArray, SalinityIdx, Kokkos::ALL, Kokkos::ALL); VertCoord *DefVertCoord = VertCoord::getDefault(); - OMEGA_SCOPE(LocMinLayerCell, DefVertCoord->MinLayerCell); - OMEGA_SCOPE(LocNAccumSteps, NAccumSteps); - OMEGA_SCOPE(LocAvgSfcSalinity, OcnToCpl.AvgSfcSalinity); - OMEGA_SCOPE(LocAvgSfcTemp, OcnToCpl.AvgSfcTemperature); - OMEGA_SCOPE(LocAvgSfcVelZonal, OcnToCpl.AvgSfcVelocityZonal); - OMEGA_SCOPE(LocAvgSfcVelMerid, OcnToCpl.AvgSfcVelocityMerid); + OMEGA_SCOPE(LocMinLayerCell, DefVertCoord->MinLayerCell); + OMEGA_SCOPE(LocAvgSfcSalinity, AvgSfcSalinity); + OMEGA_SCOPE(LocAvgSfcTemp, AvgSfcTemperature); + OMEGA_SCOPE(LocAvgSfcVelZonal, AvgSfcVelocityZonal); + OMEGA_SCOPE(LocAvgSfcVelMerid, AvgSfcVelocityMerid); // TODO: Implement vector reconsturction for velocity field. constexpr Real ConstSfcVelocity = 1e-4; @@ -282,47 +317,51 @@ void SfcCoupling::updateExportFields(const OceanState *State, // Update the averaged fields using Welford's online algorithm LocAvgSfcTemp(ICell) = updateAverage( - LocAvgSfcTemp(ICell), Temperature(ICell, KSfc), LocNAccumSteps); + LocAvgSfcTemp(ICell), Temperature(ICell, KSfc), NAccumSteps); LocAvgSfcSalinity(ICell) = updateAverage( - LocAvgSfcSalinity(ICell), Salinity(ICell, KSfc), LocNAccumSteps); + LocAvgSfcSalinity(ICell), Salinity(ICell, KSfc), NAccumSteps); LocAvgSfcVelZonal(ICell) = updateAverage( - LocAvgSfcVelZonal(ICell), ConstSfcVelocity, LocNAccumSteps); + LocAvgSfcVelZonal(ICell), ConstSfcVelocity, NAccumSteps); LocAvgSfcVelMerid(ICell) = updateAverage( - LocAvgSfcVelMerid(ICell), ConstSfcVelocity, LocNAccumSteps); + LocAvgSfcVelMerid(ICell), ConstSfcVelocity, NAccumSteps); }); - - NAccumSteps++; } -CplToOcnFields::CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh) - : SfcStressZonal("SfcStressZonal" + Suffix, Mesh->NCellsOwned), - SfcStressMerid("SfcStressMeridional" + Suffix, Mesh->NCellsOwned) {} - -OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) - : AvgSfcTemperature("AvgSfcTemperature" + Suffix, Mesh->NCellsOwned), - AvgSfcSalinity("AvgSfcSalinity" + Suffix, Mesh->NCellsOwned), - AvgSfcVelocityZonal("AvgSfcVelocityZonal" + Suffix, Mesh->NCellsOwned), - AvgSfcVelocityMerid("AvgSfcVelocityMeridional" + Suffix, - Mesh->NCellsOwned), - InstSshCellH("InstSshCellH" + Suffix, Mesh->NCellsOwned) { - - // Kokkok views created with a label are zero-initialized by default. - // We reset the feilds here anyway to be explicit about the fact that the - // OcnToCpl fields need to being a coupling interval with all zeros. - resetFields(); +// Conversion from conservative [C] to in-situ [K] temperature is done on +// the device, as well as conversion from practical to absolute salinity. +// This allows the expensive unit conversions to be called on device, once +// per coupling interval. Conversion is written to scratch buffers, to +// keep units consitent in time. Special care is paid to guard extenal +// access to the device arrays, so that inconsitent units between the device +// and host mirrors are not exposed to the rest of the code. +void OcnToCplFields::copyToHost() { - AvgSfcTemperatureH = createHostMirrorCopy(AvgSfcTemperature); - AvgSfcSalinityH = createHostMirrorCopy(AvgSfcSalinity); - AvgSfcVelocityZonalH = createHostMirrorCopy(AvgSfcVelocityZonal); - AvgSfcVelocityMeridH = createHostMirrorCopy(AvgSfcVelocityMerid); -} + // Convert averaged Conservative Temp to in-situ (approx by potential + // temp at P=0) Kelvin, once per coupling interval, on device. Written + // into a scratch buffer so AvgSfcTemperature always stays deg C. + // A local Teos10Eos is constructed here rather than reusing Eos's + // instance: calcPtFromCt() needs no config-derived state (fixed + // polynomial coefficients only), so this avoids exposing Eos internals. + EosType LocEosChoice = Eos::getInstance()->EosChoice; + Teos10Eos LocTeos10(VertCoord::getDefault()); + OMEGA_SCOPE(LocAvgSfcTemp, AvgSfcTemperature); + OMEGA_SCOPE(LocAvgSfcSalinity, AvgSfcSalinity); + OMEGA_SCOPE(LocInSituTemp, InSituTempScratch); -void OcnToCplFields::copyToHost() { + parallelFor( + {(int)AvgSfcTemperature.extent(0)}, KOKKOS_LAMBDA(int Cell) { + const Real Ct = LocAvgSfcTemp(Cell); + const Real Sa = LocAvgSfcSalinity(Cell); + const Real Pt = LocEosChoice == EosType::Teos10Eos + ? LocTeos10.calcPtFromCt(Sa, Ct) + : Ct; + LocInSituTemp(Cell) = Pt + TkFrz; + }); - deepCopy(AvgSfcTemperatureH, AvgSfcTemperature); + deepCopy(AvgSfcTemperatureH, InSituTempScratch); deepCopy(AvgSfcSalinityH, AvgSfcSalinity); deepCopy(AvgSfcVelocityZonalH, AvgSfcVelocityZonal); deepCopy(AvgSfcVelocityMeridH, AvgSfcVelocityMerid); diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 2d123be717c2..137a89ffb16c 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -56,42 +56,53 @@ class CplToOcnFields { // o2x: Ocean to Coupler class OcnToCplFields { public: - ///< So_t [deg C] - Array1DReal AvgSfcTemperature; + ///< So_t [K], in-situ approx (potential temp at P=0) HostArray1DReal AvgSfcTemperatureH; - ///< So_s [g kg^-1] - Array1DReal AvgSfcSalinity; + /// TODO: Export practical salinity (unitless) to coupler + ///< So_s [g kg^-1], absolute salinity HostArray1DReal AvgSfcSalinityH; ///< So_u [m s^-1] - Array1DReal AvgSfcVelocityZonal; HostArray1DReal AvgSfcVelocityZonalH; ///< So_v [m s^-1] - Array1DReal AvgSfcVelocityMerid; HostArray1DReal AvgSfcVelocityMeridH; ///< So_ssh [m] /// instantaneous field, so no device mirror is needed HostArray1DReal InstSshCellH; - // Refresh host mirrors from their devide counterparts + // Accumulate one ocean timestep's contribution to the running averages + void updateAverages(const OceanState *State, const Array3DReal &TracerArray, + I4 NAccumSteps, I4 NCellsOwned); + + // Copy device arrays into their host mirrros and do unit conversion. void copyToHost(); // Reset all fields to 0 void resetFields(); OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); + + private: + // Device arrays are private to prevent any code from mirroring to host + // except through copyToHost(), so that the different unit bewteen the + // device and host mirrors of temperature and salinity are not exposed to + // the rest of the code. + Array1DReal AvgSfcTemperature; // [C], conservative temperature + Array1DReal AvgSfcSalinity; // [g kg^-1], absolute salinity + Array1DReal AvgSfcVelocityZonal; + Array1DReal AvgSfcVelocityMerid; + + // Scratch buffer for the in-situ Kelvin conversion in copyToHost() + Array1DReal InSituTempScratch; // [K], in-situ approx (potential temp at P=0) }; /// A class for interfacing with the coupler /// The SfcCoupling class provides a container for the variables exchanged -/// to (o2x) and from (x2o) the coupler. It containes methods to handle the -/// import and export of raw data from the coupler, unit conversion of said -/// data, application of that data to the model state, and accumulation of the -/// data for avergaing or sumation over multiple ocean time steps. +/// to (o2x) and from (x2o) the coupler. class SfcCoupling { private: diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index c2a27155a2a3..f7b59ec80463 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -5,6 +5,7 @@ #include "Eos.h" #include "Field.h" #include "Forcing.h" +#include "GlobalConstants.h" #include "Halo.h" #include "HorzMesh.h" #include "IO.h" @@ -91,6 +92,13 @@ int initSfcCouplingTest(const std::string &MeshFile) { Config("Omega"); Config::readAll("omega.yml"); + // Force ConstantEos so calcPtFromCt() is a no-op, letting tests below use + // simple mocked Temperature/Salinity without the TEOS-10 polynomial + Config *OmegaConfig = Config::getOmegaConfig(); + Config EosConfig("Eos"); + OmegaConfig->get(EosConfig); + EosConfig.set("EosType", std::string("constant")); + // Initialize time stepper and get model clock TimeStepper::init1(); TimeStepper *DefStepper = TimeStepper::getDefault(); @@ -121,6 +129,7 @@ int initSfcCouplingTest(const std::string &MeshFile) { } Eos::init(); + Forcing::init(); Tracers::init(); @@ -288,17 +297,19 @@ int testUpdateExportFields(const I4 NSteps) { DefCoupling->getNAccumSteps(), NSteps); } - auto AvgTempH = - createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcTemperature); - auto AvgSalinH = createHostMirrorCopy(DefCoupling->OcnToCpl.AvgSfcSalinity); + // copyToHost() is the only sanctioned way to read the averages; it also + // converts temp to in-situ Kelvin (identity CT->PT w/ ConstantEos) + DefCoupling->OcnToCpl.copyToHost(); + auto &AvgTempH = DefCoupling->OcnToCpl.AvgSfcTemperatureH; + auto &AvgSalinH = DefCoupling->OcnToCpl.AvgSfcSalinityH; // Only accumulated round-off error (no discretization/truncation error), // so use a tighter tolerance than tests comparing against analytic // solutions of numerical operators. - Real RTol = sizeof(Real) == 4 ? 1e-5 : 1e-10; - Real StepOffset = static_cast(NSteps - 1) / 2.0; - HostArray1DReal ExpectedTemp = - makeCellVarryingArray("ExpectedTemp", NCells, TempBase + StepOffset); + Real RTol = sizeof(Real) == 4 ? 1e-5 : 1e-10; + Real StepOffset = static_cast(NSteps - 1) / 2.0; + HostArray1DReal ExpectedTemp = makeCellVarryingArray( + "ExpectedTemp", NCells, TempBase + StepOffset + TkFrz); HostArray1DReal ExpectedSalin = makeCellVarryingArray("ExpectedSalin", NCells, SalinBase + StepOffset); @@ -354,6 +365,7 @@ int testExportToCoupler(const CouplingLayout Layout) { Err += SfcCoupling::init(CouplingParams); SfcCoupling *DefCoupling = SfcCoupling::getDefault(); + OceanState *DefState = OceanState::getDefault(); VertCoord *DefVertCoord = VertCoord::getDefault(); int NCells = DefCoupling->NCellsOwned; @@ -365,8 +377,6 @@ int testExportToCoupler(const CouplingLayout Layout) { int TempIdx = CouplingParams.ExportIdx.at("So_t"); int SalinIdx = CouplingParams.ExportIdx.at("So_s"); - int VelUIdx = CouplingParams.ExportIdx.at("So_u"); - int VelVIdx = CouplingParams.ExportIdx.at("So_v"); int SshIdx = CouplingParams.ExportIdx.at("So_ssh"); DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); @@ -375,17 +385,26 @@ int testExportToCoupler(const CouplingLayout Layout) { makeCellVarryingArray("ExpectedTemp", NCells, Real(TempIdx)); HostArray1DReal ExpectedSalin = makeCellVarryingArray("ExpectedSalin", NCells, Real(SalinIdx)); - HostArray1DReal ExpectedVelU = - makeCellVarryingArray("ExpectedVelU", NCells, Real(VelUIdx)); - HostArray1DReal ExpectedVelV = - makeCellVarryingArray("ExpectedVelV", NCells, Real(VelVIdx)); HostArray1DReal ExpectedSsh = makeCellVarryingArray("ExpectedSsh", NCells, Real(SshIdx)); - deepCopy(DefCoupling->OcnToCpl.AvgSfcTemperature, ExpectedTemp); - deepCopy(DefCoupling->OcnToCpl.AvgSfcSalinity, ExpectedSalin); - deepCopy(DefCoupling->OcnToCpl.AvgSfcVelocityZonal, ExpectedVelU); - deepCopy(DefCoupling->OcnToCpl.AvgSfcVelocityMerid, ExpectedVelV); + // Seed the averages through the public API: at NAccumSteps == 0, + // Welford's update reduces to AvgSfc* := mocked value, so one call sets + // the averages to exactly ExpectedTemp/ExpectedSalin. + I4 TempTracerIdx, SalinTracerIdx; + Tracers::getIndex(TempTracerIdx, "Temperature"); + Tracers::getIndex(SalinTracerIdx, "Salinity"); + + HostArray2DReal TempH = Tracers::getHostByIndex(0, TempTracerIdx); + HostArray2DReal SalinH = Tracers::getHostByIndex(0, SalinTracerIdx); + for (int Cell = 0; Cell < NCells; Cell++) { + int KSfc = DefVertCoord->MinLayerCell(Cell); + TempH(Cell, KSfc) = ExpectedTemp(Cell); + SalinH(Cell, KSfc) = ExpectedSalin(Cell); + } + Tracers::copyToDevice(0); + + DefCoupling->updateExportFields(DefState, Tracers::getAll(0)); auto SshCellOwned = Kokkos::subview(DefVertCoord->SshCell, std::pair(0, NCells)); @@ -393,25 +412,21 @@ int testExportToCoupler(const CouplingLayout Layout) { DefCoupling->exportToCoupler(); - // Check 1: exportToCoupler properly packs into OcnToCplView + // Check 1: exportToCoupler properly packs into OcnToCplView. Velocity + // is skipped here: its averaging is a hardcoded stub pending real vector + // reconstruction (see OcnToCplFields::updateAverages), not yet + // meaningful to check. + // copyToHost() converts temp to Kelvin (identity CT->PT w/ ConstantEos) int PackErr = 0; for (int Cell = 0; Cell < NCells; Cell++) { if (OcnToCplData[flatIdx(Layout, Cell, TempIdx, NCells, NExports)] != - ExpectedTemp(Cell)) { + ExpectedTemp(Cell) + TkFrz) { PackErr++; } if (OcnToCplData[flatIdx(Layout, Cell, SalinIdx, NCells, NExports)] != ExpectedSalin(Cell)) { PackErr++; } - if (OcnToCplData[flatIdx(Layout, Cell, VelUIdx, NCells, NExports)] != - ExpectedVelU(Cell)) { - PackErr++; - } - if (OcnToCplData[flatIdx(Layout, Cell, VelVIdx, NCells, NExports)] != - ExpectedVelV(Cell)) { - PackErr++; - } if (OcnToCplData[flatIdx(Layout, Cell, SshIdx, NCells, NExports)] != ExpectedSsh(Cell)) { PackErr++; @@ -432,12 +447,16 @@ int testExportToCoupler(const CouplingLayout Layout) { HostArray1DReal ZeroArray("ZeroArray", NCells); deepCopy(ZeroArray, 0.0); + // AvgSfcTemperatureH holds Kelvin, so a reset (0 degC) mirrors to TkFrz + HostArray1DReal ZeroTempArray("ZeroTempArray", NCells); + deepCopy(ZeroTempArray, TkFrz); + // Refresh OcnToCpl's own host mirrors post-reset, rather than creating // separate test-only mirrors DefCoupling->OcnToCpl.copyToHost(); int ResetErr = 0; - if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcTemperatureH, ZeroArray)) + if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcTemperatureH, ZeroTempArray)) ResetErr++; if (!arraysEqual(DefCoupling->OcnToCpl.AvgSfcSalinityH, ZeroArray)) ResetErr++; From 4205708ca370832463be7918e5a93cdf8821fedf Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Fri, 3 Jul 2026 11:05:43 -0700 Subject: [PATCH 21/27] Rename upate method and fix spelling errors --- components/omega/src/ocn/SfcCoupling.cpp | 15 +++++++-------- components/omega/src/ocn/SfcCoupling.h | 10 +++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index fcc0bf6398b7..9d5e2875174f 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -257,7 +257,7 @@ void SfcCoupling::applyImportFields(Forcing *Forcing) { void SfcCoupling::updateExportFields(const OceanState *State, const Array3DReal &TracerArray) { - OcnToCpl.updateAverages(State, TracerArray, NAccumSteps, NCellsOwned); + OcnToCpl.updateFields(State, TracerArray, NAccumSteps, NCellsOwned); NAccumSteps++; } @@ -276,7 +276,7 @@ OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) InSituTempScratch("InSituTempScratch" + Suffix, Mesh->NCellsOwned) { // Kokkok views created with a label are zero-initialized by default. - // We reset the feilds here anyway to be explicit about the fact that the + // We reset the fields here anyway to be explicit about the fact that the // OcnToCpl fields need to being a coupling interval with all zeros. resetFields(); @@ -286,10 +286,9 @@ OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) AvgSfcVelocityMeridH = createHostMirrorCopy(AvgSfcVelocityMerid); } -void OcnToCplFields::updateAverages(const OceanState *State, - const Array3DReal &TracerArray, - const I4 NAccumSteps, - const I4 NCellsOwned) { +void OcnToCplFields::updateFields(const OceanState *State, + const Array3DReal &TracerArray, + const I4 NAccumSteps, const I4 NCellsOwned) { I4 TemperatureIdx, SalinityIdx; Tracers::getIndex(TemperatureIdx, "Temperature"); @@ -334,8 +333,8 @@ void OcnToCplFields::updateAverages(const OceanState *State, // the device, as well as conversion from practical to absolute salinity. // This allows the expensive unit conversions to be called on device, once // per coupling interval. Conversion is written to scratch buffers, to -// keep units consitent in time. Special care is paid to guard extenal -// access to the device arrays, so that inconsitent units between the device +// keep units consistent in time. Special care is paid to guard external +// access to the device arrays, so that inconsistent units between the device // and host mirrors are not exposed to the rest of the code. void OcnToCplFields::copyToHost() { diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index 137a89ffb16c..c7975710e17d 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -74,10 +74,10 @@ class OcnToCplFields { HostArray1DReal InstSshCellH; // Accumulate one ocean timestep's contribution to the running averages - void updateAverages(const OceanState *State, const Array3DReal &TracerArray, - I4 NAccumSteps, I4 NCellsOwned); + void updateFields(const OceanState *State, const Array3DReal &TracerArray, + I4 NAccumSteps, I4 NCellsOwned); - // Copy device arrays into their host mirrros and do unit conversion. + // Copy device arrays into their host mirrors and do unit conversion. void copyToHost(); // Reset all fields to 0 @@ -174,7 +174,7 @@ class SfcCoupling { const TimeInterval &CouplingTimeStep, const CouplingLayout &CouplingLayout); - /// Initlaize SfcCoupling + /// Initialize SfcCoupling static int init(const CouplingInitParams &CouplingInitParams); /// Destructor - deallocates all memory and deletes an SfcCoupling @@ -192,7 +192,7 @@ class SfcCoupling { /// Get a surface coupling object by name static SfcCoupling *get(const std::string name); - /// Getter for the number of ocean timestpes accumulated over the coupling + /// Getter for the number of ocean timesteps accumulated over the coupling /// interval I4 getNAccumSteps() const; From b82337acdf919447386f0f0c394b3995a9dbd305 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 6 Jul 2026 13:17:41 -0700 Subject: [PATCH 22/27] Fix typos found in code review --- components/omega/src/ocn/SfcCoupling.cpp | 2 +- components/omega/test/ocn/SfcCouplingTest.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 9d5e2875174f..211201077688 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -277,7 +277,7 @@ OcnToCplFields::OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh) // Kokkok views created with a label are zero-initialized by default. // We reset the fields here anyway to be explicit about the fact that the - // OcnToCpl fields need to being a coupling interval with all zeros. + // OcnToCpl fields need to begin a coupling interval with all zeros. resetFields(); AvgSfcTemperatureH = createHostMirrorCopy(AvgSfcTemperature); diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index f7b59ec80463..ed6a19399975 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -68,7 +68,7 @@ HostArray1DReal makeCellVarryingArray(const std::string &Name, const int NCells, return Array; } -// Shaed index formula for packing/unpacking from rae coupler buffer +// Shared index formula for packing/unpacking from raw coupler buffer int flatIdx(const CouplingLayout Layout, const int Cell, const int Field, const int NCells, const int NFields) { if (Layout == CouplingLayout::MCT) From 1e8058c03c73dcbe51c4988e6a8fa983a23f2d27 Mon Sep 17 00:00:00 2001 From: Andrew Nolan <32367657+andrewdnolan@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:53:12 -0600 Subject: [PATCH 23/27] Apply suggestions from code review Co-authored-by: Carolyn Begeman --- components/omega/doc/devGuide/SfcCoupling.md | 2 +- components/omega/src/ocn/SfcCoupling.cpp | 34 +++++++++--------- components/omega/src/ocn/SfcCoupling.h | 21 +++++------ components/omega/test/ocn/SfcCouplingTest.cpp | 35 ++++++++++--------- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/components/omega/doc/devGuide/SfcCoupling.md b/components/omega/doc/devGuide/SfcCoupling.md index 7ea848c74fde..a3c5938d42a5 100644 --- a/components/omega/doc/devGuide/SfcCoupling.md +++ b/components/omega/doc/devGuide/SfcCoupling.md @@ -29,7 +29,7 @@ A non-default instance can be created with the static `create` method, which returns a pointer to the newly created object: ```c++ OMEGA::SfcCoupling* NewSfcCoupling = OMEGA::SfcCoupling::create( - Name, Mesh, NImportFields, NExportFields, ImportIdx, ExportIdx, + Name, Mesh, NImportFields, NExportFields, ImportIdxMap, ExportIdxMap, Stepper, CouplingTimeStep, Layout); ``` Given its name, a pointer to a named instance can be obtained at any time: diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 211201077688..f607bcadc706 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -47,8 +47,8 @@ int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { // Create the default surface coupling object and set pointer to it SfcCoupling::DefaultSfcCoupling = SfcCoupling::create( "Default", DefHorzMesh, CouplingInitParams.NImportFields, - CouplingInitParams.NExportFields, CouplingInitParams.ImportIdx, - CouplingInitParams.ExportIdx, DefTimeStepper, CplTimeStep, + CouplingInitParams.NExportFields, CouplingInitParams.ImportIdxMap, + CouplingInitParams.ExportIdxMap, DefTimeStepper, CplTimeStep, CouplingInitParams.Layout); return Err; @@ -57,14 +57,14 @@ int SfcCoupling::init(const CouplingInitParams &CouplingInitParams) { // Construct a new surface coupling object SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, const int NImportFields_, const int NExportFields_, - const std::map &ImportIdx, - const std::map &ExportIdx, + const std::map &ImportIdxMap, + const std::map &ExportIdxMap, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout) : Name(Name_), NImportFields(NImportFields_), NExportFields(NExportFields_), - ImportIdx(ImportIdx), ExportIdx(ExportIdx), CplToOcn(Name_, Mesh), - OcnToCpl(Name_, Mesh), Layout(Layout) { + ImportIdxMap(ImportIdxMap), ExportIdxMap(ExportIdxMap), + CplToOcn(Name_, Mesh), OcnToCpl(Name_, Mesh), Layout(Layout) { // Retrieve mesh cell count NCellsOwned = Mesh->NCellsOwned; @@ -88,8 +88,8 @@ SfcCoupling::SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, // it in the AllSfcCoupling map SfcCoupling *SfcCoupling::create( const std::string &Name, const HorzMesh *Mesh, const int NImportFields, - const int NExportFields, const std::map &ImportIdx, - const std::map &ExportIdx, TimeStepper *Stepper, + const int NExportFields, const std::map &ImportIdxMap, + const std::map &ExportIdxMap, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout) { // Check to see if a surface coupling of the same name already exists @@ -103,8 +103,8 @@ SfcCoupling *SfcCoupling::create( // create a new surface coupling on the heap and store it in the map of // unique_ptrs, which will manage its lifetime auto *NewSfcCoupling = - new SfcCoupling(Name, Mesh, NImportFields, NExportFields, ImportIdx, - ExportIdx, Stepper, CouplingTimeStep, Layout); + new SfcCoupling(Name, Mesh, NImportFields, NExportFields, ImportIdxMap, + ExportIdxMap, Stepper, CouplingTimeStep, Layout); AllSfcCoupling.emplace(Name, NewSfcCoupling); return NewSfcCoupling; @@ -185,8 +185,8 @@ void SfcCoupling::importFromCoupler() { } // Get import field indices for surface stress components - int TauxIdx = ImportIdx.at("Foxx_taux"); - int TauyIdx = ImportIdx.at("Foxx_tauy"); + int TauxIdx = ImportIdxMap.at("Foxx_taux"); + int TauyIdx = ImportIdxMap.at("Foxx_tauy"); // Copy Kokkos view handles auto CplToOcnView_ = CplToOcnView; @@ -215,11 +215,11 @@ void SfcCoupling::exportToCoupler() { // Copy the OcnToCpl fields to their host mirrors OcnToCpl.copyToHost(); - int TempIdx = ExportIdx.at("So_t"); - int SalinIdx = ExportIdx.at("So_s"); - int VelUIdx = ExportIdx.at("So_u"); - int VelVIdx = ExportIdx.at("So_v"); - int SshIdx = ExportIdx.at("So_ssh"); + int TempIdx = ExportIdxMap.at("So_t"); + int SalinIdx = ExportIdxMap.at("So_s"); + int VelUIdx = ExportIdxMap.at("So_u"); + int VelVIdx = ExportIdxMap.at("So_v"); + int SshIdx = ExportIdxMap.at("So_ssh"); // Copy Kokkos view handles auto OcnToCplView_ = OcnToCplView; diff --git a/components/omega/src/ocn/SfcCoupling.h b/components/omega/src/ocn/SfcCoupling.h index c7975710e17d..85a63bd44860 100644 --- a/components/omega/src/ocn/SfcCoupling.h +++ b/components/omega/src/ocn/SfcCoupling.h @@ -35,8 +35,8 @@ enum class CouplingLayout { MCT, MOAB }; struct CouplingInitParams { int NImportFields; int NExportFields; - std::map ImportIdx; - std::map ExportIdx; + std::map ImportIdxMap; + std::map ExportIdxMap; TimeInterval CouplingTimeStep; CouplingLayout Layout; }; @@ -113,8 +113,8 @@ class SfcCoupling { // Construct a new local coupling object SfcCoupling(const std::string &Name_, const HorzMesh *Mesh, const int NImportFields_, const int NExportFields_, - const std::map &ImportIdx, - const std::map &ExportIdx, + const std::map &ImportIdxMap, + const std::map &ExportIdxMap, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout); @@ -133,16 +133,17 @@ class SfcCoupling { CouplingLayout Layout; ///< Coupling layout (MCT or MOAB) // Map of import/export variable names to index in the raw data arrays - std::map ImportIdx; - std::map ExportIdx; + std::map ImportIdxMap; + std::map ExportIdxMap; public: std::string Name; I4 NCellsOwned; ///< Number of cells owned by this task - // The values below will be larger than InportIdx.size() and ExportIdx.size() - // because omega does not ingest all cpl fields (e.g. BGC, landice), yet... + // The values below will be larger than InportIdx.size() and + // ExportIdxMap.size() because omega does not ingest all cpl fields (e.g. + // BGC, landice), yet... I4 NImportFields; ///< Num of fields in the x2o pointer array I4 NExportFields; ///< Num of fields in the o2x pointer array @@ -168,8 +169,8 @@ class SfcCoupling { /// in the AllSfcCoupling map static SfcCoupling *create(const std::string &Name, const HorzMesh *Mesh, const int NImportFields, const int NExportFields, - const std::map &ImportIdx, - const std::map &ExportIdx, + const std::map &ImportIdxMap, + const std::map &ExportIdxMap, TimeStepper *Stepper, const TimeInterval &CouplingTimeStep, const CouplingLayout &CouplingLayout); diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index ed6a19399975..184ab0b1e951 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -23,8 +23,9 @@ using namespace OMEGA; struct TestSetup { - std::map ImportIdx = {{"Foxx_taux", 3}, {"Foxx_tauy", 8}}; - std::map ExportIdx = { + std::map ImportIdxMap = {{"Foxx_taux", 3}, + {"Foxx_tauy", 8}}; + std::map ExportIdxMap = { {"So_t", 2}, {"So_s", 4}, {"So_u", 6}, {"So_v", 9}, {"So_ssh", 1}}; }; @@ -40,8 +41,8 @@ CouplingInitParams mockCouplingInitParams( CouplingInitParams CouplingParams{.NImportFields = 10, .NExportFields = 10, - .ImportIdx = Setup.ImportIdx, - .ExportIdx = Setup.ExportIdx, + .ImportIdxMap = Setup.ImportIdxMap, + .ExportIdxMap = Setup.ExportIdxMap, .CouplingTimeStep = CouplingTimeStep_, .Layout = Layout}; @@ -115,7 +116,7 @@ int initSfcCouplingTest(const std::string &MeshFile) { int HaloErr = Halo::init(); if (HaloErr != 0) { Err++; - LOG_ERROR("SfcCouplingTest: Error initializing defualt halo"); + LOG_ERROR("SfcCouplingTest: Error initializing default halo"); } HorzMesh::init(ModelClock); @@ -149,8 +150,8 @@ int testImportFromCoupler(const CouplingLayout Layout) { int NImports = DefCoupling->NImportFields; int NExports = DefCoupling->NExportFields; - int TauxIdx = CouplingParams.ImportIdx.at("Foxx_taux"); - int TauyIdx = CouplingParams.ImportIdx.at("Foxx_tauy"); + int TauxIdx = CouplingParams.ImportIdxMap.at("Foxx_taux"); + int TauyIdx = CouplingParams.ImportIdxMap.at("Foxx_tauy"); std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); @@ -203,8 +204,8 @@ int testApplyImportFields() { int NImports = DefCoupling->NImportFields; int NExports = DefCoupling->NExportFields; - int TauxIdx = CouplingParams.ImportIdx.at("Foxx_taux"); - int TauyIdx = CouplingParams.ImportIdx.at("Foxx_tauy"); + int TauxIdx = CouplingParams.ImportIdxMap.at("Foxx_taux"); + int TauyIdx = CouplingParams.ImportIdxMap.at("Foxx_tauy"); std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); @@ -263,8 +264,8 @@ int testUpdateExportFields(const I4 NSteps) { int NCells = DefCoupling->NCellsOwned; - Real TempBase = static_cast(CouplingParams.ExportIdx.at("So_t")); - Real SalinBase = static_cast(CouplingParams.ExportIdx.at("So_s")); + Real TempBase = static_cast(CouplingParams.ExportIdxMap.at("So_t")); + Real SalinBase = static_cast(CouplingParams.ExportIdxMap.at("So_s")); I4 TempIdx, SalinIdx; Tracers::getIndex(TempIdx, "Temperature"); @@ -375,9 +376,9 @@ int testExportToCoupler(const CouplingLayout Layout) { std::vector CplToOcnData(NCells * NImports, 0.0); std::vector OcnToCplData(NCells * NExports, 0.0); - int TempIdx = CouplingParams.ExportIdx.at("So_t"); - int SalinIdx = CouplingParams.ExportIdx.at("So_s"); - int SshIdx = CouplingParams.ExportIdx.at("So_ssh"); + int TempIdx = CouplingParams.ExportIdxMap.at("So_t"); + int SalinIdx = CouplingParams.ExportIdxMap.at("So_s"); + int SshIdx = CouplingParams.ExportIdxMap.at("So_ssh"); DefCoupling->attachData(CplToOcnData.data(), OcnToCplData.data()); @@ -495,9 +496,9 @@ int testEraseAndGet() { TimeInterval TimeStep = DefStepper->getTimeStep(); // test creation of a non-default, named surface coupling object - SfcCoupling::create("AnotherSfcCoupling", DefMesh, 10, 12, Setup.ImportIdx, - Setup.ExportIdx, DefStepper, TimeStep, - CouplingLayout::MCT); + SfcCoupling::create("AnotherSfcCoupling", DefMesh, 10, 12, + Setup.ImportIdxMap, Setup.ExportIdxMap, DefStepper, + TimeStep, CouplingLayout::MCT); if (SfcCoupling::get("AnotherSfcCoupling")) { LOG_INFO("SfcCouplingTest: Non-default SfcCoupling retrieval PASS"); From 089f50d82cb14e6f2ed813b5bab5e68fc6f5c648 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 6 Jul 2026 14:13:49 -0700 Subject: [PATCH 24/27] Fix "being" / "begin" typo --- components/omega/src/ocn/SfcCoupling.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index f607bcadc706..7e2d8bcda644 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -375,7 +375,7 @@ void OcnToCplFields::copyToHost() { deepCopy(InstSshCellH, SSHCellOwned); } -// OcnToCpl fields need to being a coupling interval with all values set to 0. +// OcnToCpl fields need to begin a coupling interval with all values set to 0. void OcnToCplFields::resetFields() { deepCopy(AvgSfcTemperature, 0.0); deepCopy(AvgSfcSalinity, 0.0); From ada27010a83880d5b67e0b8bfab5e05446d66e59 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 6 Jul 2026 17:43:47 -0700 Subject: [PATCH 25/27] Access `MinLayerCell` on host in manual for loops --- components/omega/test/ocn/SfcCouplingTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/omega/test/ocn/SfcCouplingTest.cpp b/components/omega/test/ocn/SfcCouplingTest.cpp index 184ab0b1e951..78964f622364 100644 --- a/components/omega/test/ocn/SfcCouplingTest.cpp +++ b/components/omega/test/ocn/SfcCouplingTest.cpp @@ -278,7 +278,7 @@ int testUpdateExportFields(const I4 NSteps) { HostArray2DReal SalinH = Tracers::getHostByIndex(0, SalinIdx); for (int Cell = 0; Cell < NCells; Cell++) { - int KSfc = DefVertCoord->MinLayerCell(Cell); + int KSfc = DefVertCoord->MinLayerCellH(Cell); TempH(Cell, KSfc) = TempBase + Cell + CurrStep; SalinH(Cell, KSfc) = SalinBase + Cell + CurrStep; @@ -399,7 +399,7 @@ int testExportToCoupler(const CouplingLayout Layout) { HostArray2DReal TempH = Tracers::getHostByIndex(0, TempTracerIdx); HostArray2DReal SalinH = Tracers::getHostByIndex(0, SalinTracerIdx); for (int Cell = 0; Cell < NCells; Cell++) { - int KSfc = DefVertCoord->MinLayerCell(Cell); + int KSfc = DefVertCoord->MinLayerCellH(Cell); TempH(Cell, KSfc) = ExpectedTemp(Cell); SalinH(Cell, KSfc) = ExpectedSalin(Cell); } From d0ac98400b367af626dd2c564455c940d4e2fde8 Mon Sep 17 00:00:00 2001 From: Andrew Nolan <32367657+andrewdnolan@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:23:06 -0600 Subject: [PATCH 26/27] Apply suggestions from code review Co-authored-by: alicebarthel <36741014+alicebarthel@users.noreply.github.com> --- components/omega/src/ocn/SfcCoupling.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/omega/src/ocn/SfcCoupling.cpp b/components/omega/src/ocn/SfcCoupling.cpp index 7e2d8bcda644..42f362474c3d 100644 --- a/components/omega/src/ocn/SfcCoupling.cpp +++ b/components/omega/src/ocn/SfcCoupling.cpp @@ -357,7 +357,7 @@ void OcnToCplFields::copyToHost() { const Real Pt = LocEosChoice == EosType::Teos10Eos ? LocTeos10.calcPtFromCt(Sa, Ct) : Ct; - LocInSituTemp(Cell) = Pt + TkFrz; + LocInSituTemp(Cell) = Pt + TkFrz; // C to K temperature conversion }); deepCopy(AvgSfcTemperatureH, InSituTempScratch); @@ -377,9 +377,9 @@ void OcnToCplFields::copyToHost() { // OcnToCpl fields need to begin a coupling interval with all values set to 0. void OcnToCplFields::resetFields() { - deepCopy(AvgSfcTemperature, 0.0); - deepCopy(AvgSfcSalinity, 0.0); - deepCopy(AvgSfcVelocityZonal, 0.0); - deepCopy(AvgSfcVelocityMerid, 0.0); + deepCopy(AvgSfcTemperature, 0.0_Real); + deepCopy(AvgSfcSalinity, 0.0_Real); + deepCopy(AvgSfcVelocityZonal, 0.0_Real); + deepCopy(AvgSfcVelocityMerid, 0.0_Real); } } // namespace OMEGA From c48126fc06402596e7cc4761a3aaf79a34fe0906 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 9 Jul 2026 15:53:55 -0700 Subject: [PATCH 27/27] Switch to 8 tasks (from 1) for SFCOUPLING CTEST --- components/omega/test/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/omega/test/CMakeLists.txt b/components/omega/test/CMakeLists.txt index 62bca3fc945d..535e7c71aefd 100644 --- a/components/omega/test/CMakeLists.txt +++ b/components/omega/test/CMakeLists.txt @@ -560,10 +560,9 @@ add_omega_test( # SurfaceCouling test ##################### -# TODO: no halo exhange. Worth testnig with more than 1 task? add_omega_test( SFCCOUPLING_TEST testSfcCoupling.exe ocn/SfcCouplingTest.cpp - "-n;1" + "-n;8" )