From e22ff863f6f840f80cda8325d9e0c82ac6ff3ed7 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Wed, 13 May 2026 07:05:04 -0600 Subject: [PATCH 1/6] Add first pass at coupling design document --- components/omega/doc/design/Coupling.md | 279 ++++++++++++++++++++++++ components/omega/doc/index.md | 1 + 2 files changed, 280 insertions(+) create mode 100644 components/omega/doc/design/Coupling.md diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md new file mode 100644 index 000000000000..c6f0c1baece1 --- /dev/null +++ b/components/omega/doc/design/Coupling.md @@ -0,0 +1,279 @@ +(omega-design-ocn-coupler)= +# Surface Coupling + +## 1 Overview + +The `SurfaceCoupling` class manages all fields exchanged between Omega and +the coupled system (via MCT or MOAB drivers). It owns import and export +coupling field arrays, unpacks driver attribute-vector data into typed +Omega arrays, applies imported fields to internal forcing state, +accumulates export fields over each coupling interval, and repacks them +for the driver. The design cleanly separates driver-specific operations +(handled in a Fortran/C++ bridge layer) from the C++ class, is agnostic +to the underlying driver architecture, and is extensible for future +fields (BGC, waves, land-ice). + +## 2 Requirements + +### 2.1 Requirement: Own coupling field storage + +The class must own all coupling fields as `Array1DReal` arrays indexed +over ocean cells, consistent with other Omega field storage. + +### 2.2 Requirement: Clean driver boundary + +The driver interface layer (Fortran/C++ bridge) must handle all +driver-specific operations (index lookups, attribute vector layouts, +MCT/MOAB protocol). The C++ class receives and returns plain arrays of +doubles with no direct dependencies on MCT, MOAB, or other coupling +infrastructure. + +### 2.3 Requirement: Field system integration + +All coupling fields must be registerable with Omega's Field system for +diagnostic output. + +### 2.4 Requirement: Per-coupling-interval operations + +The class must provide methods to import fields, apply them to forcing +state, accumulate export quantities each ocean timestep, export +accumulated fields, and reset accumulators. + +### 2.5 Requirement: Driver architecture agnosticism + +The class must support both MCT and MOAB layouts without conditional +compilation. The data layout (MCT column-major vs MOAB field-major) is +configured at runtime through the bridge layer. + +### 2.6 Requirement: Coupling interval tracking + +The class must track the coupling interval using Omega's `Alarm` system, +creating an internal periodic alarm at initialization to manage when +per-coupling-interval operations should occur. + +### 2.7 Requirement: Coupler conversion layer + +Private conversion methods must handle unit conversions and state +variable transformations between Omega's internal representation and +coupler expectations (e.g., conservative temperature ↔ potential +temperature). + +### 2.8 Requirement: Extensible design + +Adding new coupling fields must require only localized changes: add +array members and update import/export logic. No restructuring of class +interfaces or method signatures should be needed. + +## 3 Algorithmic Formulation + +No algorithms are required. + +## 4 Design + +### 4.1 Data types and parameters + +#### 4.1.1 Parameters + +An enum class specifies the coupled driver layout: +```cpp +enum class CouplingLayout { MCT, MOAB }; +``` + +#### 4.1.2 Class/structs/data types + +`SurfaceCoupling` lives in `src/ocn/` alongside `AuxiliaryState` and +`OceanState`. It has no direct driver dependencies. The bridge layer in +`src/drivers/coupled/` is the only code that calls it. + +```cpp +class SurfaceCoupling { + public: + // Import fields (x2o): coupler → ocean + Array1DReal WindStressZonal; ///< Foxx_taux [N m⁻²] + Array1DReal WindStressMeridional; ///< Foxx_tauy [N m⁻²] + Array1DReal SWHeatFlux; ///< Foxx_swnet [W m⁻²] + Array1DReal LatentHeatFlux; ///< Foxx_lat [W m⁻²] + Array1DReal SensibleHeatFlux; ///< Foxx_sen [W m⁻²] + Array1DReal LWHeatFluxUp; ///< Foxx_lwup [W m⁻²] + Array1DReal LWHeatFluxDown; ///< Faxa_lwdn [W m⁻²] + Array1DReal SeaIceHeatFlux; ///< Fioi_melth [W m⁻²] + Array1DReal IcebergHeatFlux; ///< Fioi_bergh [W m⁻²] + Array1DReal SnowFlux; ///< Faxa_snow [kg m⁻² s⁻¹] + Array1DReal RainFlux; ///< Faxa_rain [kg m⁻² s⁻¹] + Array1DReal EvaporationFlux; ///< Foxx_evap [kg m⁻² s⁻¹] + Array1DReal SeaIceFreshWaterFlux; ///< Fioi_meltw [kg m⁻² s⁻¹] + Array1DReal IcebergFreshWaterFlux; ///< Fioi_bergw [kg m⁻² s⁻¹] + Array1DReal SeaIceSalinityFlux; ///< Fioi_salt [kg m⁻² s⁻¹] + Array1DReal RiverRunoffFlux; ///< Foxx_rofl [kg m⁻² s⁻¹] + Array1DReal IceRunoffFlux; ///< Foxx_rofi [kg m⁻² s⁻¹] + Array1DReal IceFraction; ///< Si_ifrac [-] + Array1DReal SeaIcePressure; ///< Si_bpress [Pa] + Array1DReal AtmosphericPressure; ///< Sa_pslv [Pa] + + // Export fields (o2x): ocean → coupler + Array1DReal SurfaceTemperature; ///< So_t [°C] + Array1DReal SurfaceSalinity; ///< So_s [g kg⁻¹] + Array1DReal SurfaceVelocityZonal; ///< So_u [m s⁻¹] + Array1DReal SurfaceVelocityMeridional; ///< So_v [m s⁻¹] + Array1DReal SSH; ///< So_ssh [m] + Array1DReal SSHGradientZonal; ///< So_dhdx [m m⁻¹] + Array1DReal SSHGradientMeridional; ///< So_dhdy [m m⁻¹] + Array1DReal SeaIceFormationHeat; ///< Fioo_q [W m⁻²] + Array1DReal FrazilIceMass; ///< Fioo_frazil [kg m⁻² s⁻¹] + Array1DReal FreshWaterHeatFlux; ///< Faoo_h2otemp[W m⁻²] + + // Public methods + int importFromCoupler(const Real *x2oData, int NFields, int NCells); + int exportToCoupler(Real *o2xData, int NFields, int NCells); + int applyImportedState(AuxiliaryState *AuxState); + int accumulateExportState(const OceanState *State, + const Array3DReal &TracerArray, + int TimeLevel); + void resetAccumulators(); + + private: + int NAccumSteps = 0; + Alarm CouplingAlarm; + std::map ImportIdx; + std::map ExportIdx; + CouplingLayout DataLayout; + + static Real potTempToConservTemp(Real PotTemp, Real Salinity, Real Pressure); + static Real conservTempToPotTemp(Real ConsTemp, Real Salinity, Real Pressure); +}; +``` + +### 4.2 Methods + +#### 4.2.1 Creation + +The `create` method allocates import/export arrays sized to `NCellsAll`, stores +the field index maps and `DataLayout`, creates the coupling `Alarm`, and calls +`registerFields`. After creation, `create` internally calls +`accumulateExportState` once to populate the initial export state. + +```cpp +SurfaceCoupling *SurfaceCoupling::create( + const std::string &Name, int OcnId, const HorzMesh *Mesh, + const std::map &ImportIdx, + const std::map &ExportIdx, + const TimeInterval &CplInterval, CouplingLayout Layout); +``` + +#### 4.2.2 Retrieval + +```cpp +SurfaceCoupling *SurfaceCoupling::getDefault(); +SurfaceCoupling *SurfaceCoupling::get(const std::string &Name); +``` + +#### 4.2.3 Import and Export + +`importFromCoupler` unpacks the driver array into typed member arrays using +the name→column-index map. The stride calculation is layout-dependent: MCT +uses `col*NCells + i`, MOAB uses `i*NFields + col`. Unit conversions (e.g., +potential → conservative temperature, shortwave clamping) are applied inline. + +`exportToCoupler` packs export arrays into the driver array. State fields +are divided by `NAccumSteps` (interval mean); flux fields are packed directly +(interval total). + +`applyImportedState` copies imported fields into the appropriate locations in +`AuxiliaryState` for use by ocean physics. + +`accumulateExportState` adds current-step values to running sums each ocean +timestep. + +`resetAccumulators` zeroes all export arrays and sets `NAccumSteps = 0`. + +#### 4.2.4 Conversion Methods + +Temperature conversions between conservative (Omega internal) and potential +(coupler expectation) are handled as private static methods. Simple +constant-factor conversions are applied directly in import/export methods. + +#### 4.2.5 Destruction and removal + +```cpp +void SurfaceCoupling::erase(const std::string &Name); +void SurfaceCoupling::clear(); +``` + +### 4.3 Driver Interface Bridge + +The bridge layer in `src/drivers/coupled/` provides `extern "C"` entry points +called from the Fortran driver (`ocn_comp_mct.F90` or `ocn_comp_moab.F90`). +The coupling interval and ocean timestep are independent: the driver calls +run once per coupling interval; the ocean takes many timesteps within it. + +#### 4.3.1 Name-based Field Index Mapping + +At startup, the Fortran bridge populates parallel arrays of Omega field names +and driver column indices, then passes them to `omega_ocn_init`. C++ builds +`std::map` objects for import/export lookups. This approach +is driver-agnostic: MCT and MOAB differ only in how column indices are +obtained. Field names are passed as fixed-length null-terminated character +arrays (standard Fortran-C interop), parsed by stepping through the flat +array at stride `FieldNameLen`. + +### 4.4 Interval Accumulation + +Export state fields are accumulated each ocean timestep and divided by +`NAccumSteps` at export time (interval mean). Export flux fields are +accumulated and packed directly without dividing (interval total). +`resetAccumulators` is called by `omega_ocn_export` after packing. + +### 4.5 Extensibility + +To add a new coupling field: +1. Add an `Array1DReal` member to `SurfaceCoupling.h` +2. Add the field registration in `registerFields()` +3. Add an unpack/pack call in `importFromCoupler`/`exportToCoupler` +4. Add the name/index entry in the Fortran bridge file +5. Fill the field in `applyImportedState()` or `accumulateExportState()` + +No changes to method signatures or ordering constraints are needed. + +## 5 Verification and Testing + +### 5.1 Test: Import/export round-trip + +Create synthetic coupling arrays with known values, call +`importFromCoupler`, verify member arrays match expected values. Pack +export arrays and verify the output matches. Test both MCT and MOAB +layouts. + - tests requirement 2.1, 2.2 + +### 5.2 Test: Interval averaging for state fields + +Accumulate the same state values over multiple simulated timesteps, +export, and verify the output equals the mean. + - tests requirement 2.4 + +### 5.3 Test: Interval summation for flux fields + +Accumulate flux values over multiple timesteps, export, and verify the +output equals the total sum (not divided). + - tests requirement 2.4 + +### 5.4 Test: Name-keyed field mapping + +Build the name→index map with different fill orders and verify +import/export produces correct results regardless of order. Request an +unknown field name and verify error logging. + - tests requirement 2.2, 2.8 + +### 5.5 Test: Coupling alarm + +Create a `SurfaceCoupling` with a known coupling interval. Advance the +simulation clock and verify the coupling alarm rings at the expected +interval boundaries and does not ring between them. + - tests requirement 2.6 + +### 5.6 Test: Coupling loop integration + +Simulate a complete coupling interval: import, run multiple ocean +timesteps with accumulation, export, and reset. Verify the full +import → accumulate → export → reset cycle produces correct results and +that the accumulators are properly zeroed for the next interval. + - tests requirement 2.4, 2.6 diff --git a/components/omega/doc/index.md b/components/omega/doc/index.md index 6712bb3e7f8d..6899cb0756ac 100644 --- a/components/omega/doc/index.md +++ b/components/omega/doc/index.md @@ -110,6 +110,7 @@ design/OmegaV0ShallowWater design/OmegaV1GoverningEqns design/Broadcast design/Config +design/Coupling design/DataTypes design/Decomp design/Driver From c8445bf023987d631b32d9d05a65d80cfb2375c4 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Wed, 13 May 2026 10:04:06 -0600 Subject: [PATCH 2/6] Rename potential temperature to in situ --- components/omega/doc/design/Coupling.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md index c6f0c1baece1..4a12d728128f 100644 --- a/components/omega/doc/design/Coupling.md +++ b/components/omega/doc/design/Coupling.md @@ -55,7 +55,7 @@ per-coupling-interval operations should occur. Private conversion methods must handle unit conversions and state variable transformations between Omega's internal representation and -coupler expectations (e.g., conservative temperature ↔ potential +coupler expectations (e.g., conservative temperature ↔ in situ temperature). ### 2.8 Requirement: Extensible design @@ -172,7 +172,7 @@ SurfaceCoupling *SurfaceCoupling::get(const std::string &Name); `importFromCoupler` unpacks the driver array into typed member arrays using the name→column-index map. The stride calculation is layout-dependent: MCT uses `col*NCells + i`, MOAB uses `i*NFields + col`. Unit conversions (e.g., -potential → conservative temperature, shortwave clamping) are applied inline. +in situ → conservative temperature, shortwave clamping) are applied inline. `exportToCoupler` packs export arrays into the driver array. State fields are divided by `NAccumSteps` (interval mean); flux fields are packed directly @@ -188,7 +188,7 @@ timestep. #### 4.2.4 Conversion Methods -Temperature conversions between conservative (Omega internal) and potential +Temperature conversions between conservative (Omega internal) and in situ (coupler expectation) are handled as private static methods. Simple constant-factor conversions are applied directly in import/export methods. From c6877322bca318369ff9249bfad8737ad9373203 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Wed, 20 May 2026 15:49:49 -0600 Subject: [PATCH 3/6] Adress reviewers comments --- components/omega/doc/design/Coupling.md | 66 ++++++++++++++----------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md index 4a12d728128f..829bfee91691 100644 --- a/components/omega/doc/design/Coupling.md +++ b/components/omega/doc/design/Coupling.md @@ -35,8 +35,9 @@ diagnostic output. ### 2.4 Requirement: Per-coupling-interval operations -The class must provide methods to import fields, apply them to forcing -state, accumulate export quantities each ocean timestep, export +The class must provide methods to import fields, pass the received +import fields to the appropriate forcing class member variables, +accumulate export quantities each ocean timestep, export accumulated fields, and reset accumulators. ### 2.5 Requirement: Driver architecture agnosticism @@ -89,26 +90,26 @@ enum class CouplingLayout { MCT, MOAB }; class SurfaceCoupling { public: // Import fields (x2o): coupler → ocean - Array1DReal WindStressZonal; ///< Foxx_taux [N m⁻²] - Array1DReal WindStressMeridional; ///< Foxx_tauy [N m⁻²] - Array1DReal SWHeatFlux; ///< Foxx_swnet [W m⁻²] - Array1DReal LatentHeatFlux; ///< Foxx_lat [W m⁻²] - Array1DReal SensibleHeatFlux; ///< Foxx_sen [W m⁻²] - Array1DReal LWHeatFluxUp; ///< Foxx_lwup [W m⁻²] - Array1DReal LWHeatFluxDown; ///< Faxa_lwdn [W m⁻²] - Array1DReal SeaIceHeatFlux; ///< Fioi_melth [W m⁻²] - Array1DReal IcebergHeatFlux; ///< Fioi_bergh [W m⁻²] - Array1DReal SnowFlux; ///< Faxa_snow [kg m⁻² s⁻¹] - Array1DReal RainFlux; ///< Faxa_rain [kg m⁻² s⁻¹] - Array1DReal EvaporationFlux; ///< Foxx_evap [kg m⁻² s⁻¹] - Array1DReal SeaIceFreshWaterFlux; ///< Fioi_meltw [kg m⁻² s⁻¹] - Array1DReal IcebergFreshWaterFlux; ///< Fioi_bergw [kg m⁻² s⁻¹] - Array1DReal SeaIceSalinityFlux; ///< Fioi_salt [kg m⁻² s⁻¹] - Array1DReal RiverRunoffFlux; ///< Foxx_rofl [kg m⁻² s⁻¹] - Array1DReal IceRunoffFlux; ///< Foxx_rofi [kg m⁻² s⁻¹] - Array1DReal IceFraction; ///< Si_ifrac [-] - Array1DReal SeaIcePressure; ///< Si_bpress [Pa] - Array1DReal AtmosphericPressure; ///< Sa_pslv [Pa] + Array1DReal SurfaceStressZonal; ///< Foxx_taux [N m⁻²] + Array1DReal SurfaceStressMeridional; ///< Foxx_tauy [N m⁻²] + Array1DReal SWHeatFlux; ///< Foxx_swnet [W m⁻²] + Array1DReal LatentHeatFlux; ///< Foxx_lat [W m⁻²] + Array1DReal SensibleHeatFlux; ///< Foxx_sen [W m⁻²] + Array1DReal LWHeatFluxUp; ///< Foxx_lwup [W m⁻²] + Array1DReal LWHeatFluxDown; ///< Faxa_lwdn [W m⁻²] + Array1DReal SeaIceHeatFlux; ///< Fioi_melth [W m⁻²] + Array1DReal IcebergHeatFlux; ///< Fioi_bergh [W m⁻²] + Array1DReal SnowFlux; ///< Faxa_snow [kg m⁻² s⁻¹] + Array1DReal RainFlux; ///< Faxa_rain [kg m⁻² s⁻¹] + Array1DReal EvaporationFlux; ///< Foxx_evap [kg m⁻² s⁻¹] + Array1DReal SeaIceFreshWaterFlux; ///< Fioi_meltw [kg m⁻² s⁻¹] + Array1DReal IcebergFreshWaterFlux; ///< Fioi_bergw [kg m⁻² s⁻¹] + Array1DReal SeaIceSalinityFlux; ///< Fioi_salt [kg m⁻² s⁻¹] + Array1DReal RiverRunoffFlux; ///< Foxx_rofl [kg m⁻² s⁻¹] + Array1DReal IceRunoffFlux; ///< Foxx_rofi [kg m⁻² s⁻¹] + Array1DReal IceFraction; ///< Si_ifrac [-] + Array1DReal SeaIcePressure; ///< Si_bpress [Pa] + Array1DReal AtmosphericPressure; ///< Sa_pslv [Pa] // Export fields (o2x): ocean → coupler Array1DReal SurfaceTemperature; ///< So_t [°C] @@ -147,7 +148,7 @@ class SurfaceCoupling { #### 4.2.1 Creation -The `create` method allocates import/export arrays sized to `NCellsAll`, stores +The `create` method allocates import/export arrays sized to `NCellsOwned`, stores the field index maps and `DataLayout`, creates the coupling `Alarm`, and calls `registerFields`. After creation, `create` internally calls `accumulateExportState` once to populate the initial export state. @@ -172,7 +173,10 @@ SurfaceCoupling *SurfaceCoupling::get(const std::string &Name); `importFromCoupler` unpacks the driver array into typed member arrays using the name→column-index map. The stride calculation is layout-dependent: MCT uses `col*NCells + i`, MOAB uses `i*NFields + col`. Unit conversions (e.g., -in situ → conservative temperature, shortwave clamping) are applied inline. +in situ → conservative temperature) and corrections (e.g. forcing shortwave +radiation to be positive) are applied inline. The corrections are applied to +address numerical issues, not and physical one, in the off chance monotonicity +is not preserved during remapping. `exportToCoupler` packs export arrays into the driver array. State fields are divided by `NAccumSteps` (interval mean); flux fields are packed directly @@ -188,9 +192,10 @@ timestep. #### 4.2.4 Conversion Methods -Temperature conversions between conservative (Omega internal) and in situ -(coupler expectation) are handled as private static methods. Simple -constant-factor conversions are applied directly in import/export methods. +Temperature conversion between conservative (Omega internal) and in situ +(coupler expectation) is handled as private static methods. Similar handling +will occur for salinity with conversion between absolute (Omega internal) and +practical (MPAS-Seaice expectation). #### 4.2.5 Destruction and removal @@ -202,9 +207,10 @@ void SurfaceCoupling::clear(); ### 4.3 Driver Interface Bridge The bridge layer in `src/drivers/coupled/` provides `extern "C"` entry points -called from the Fortran driver (`ocn_comp_mct.F90` or `ocn_comp_moab.F90`). -The coupling interval and ocean timestep are independent: the driver calls -run once per coupling interval; the ocean takes many timesteps within it. +called from the Fortran driver (`ocn_comp_mct.F90`). The coupling interval +and ocean timestep are independent: the driver calls `ocn_run_mct` once per +coupling interval; the ocean *can* take multiple timesteps within the coupling +interval. #### 4.3.1 Name-based Field Index Mapping From e4b2ae2eb4f3746076f55964d2db84a4407cd8db Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 22 Jun 2026 11:54:18 -0600 Subject: [PATCH 4/6] Flesh out run loop and integration with forcing --- components/omega/doc/design/Coupling.md | 60 +++++++++++++++++++++---- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md index 829bfee91691..aab2a317d804 100644 --- a/components/omega/doc/design/Coupling.md +++ b/components/omega/doc/design/Coupling.md @@ -80,6 +80,8 @@ An enum class specifies the coupled driver layout: enum class CouplingLayout { MCT, MOAB }; ``` +The initial implementation will not have any runtime configuration options. + #### 4.1.2 Class/structs/data types `SurfaceCoupling` lives in `src/ocn/` alongside `AuxiliaryState` and @@ -168,7 +170,16 @@ SurfaceCoupling *SurfaceCoupling::getDefault(); SurfaceCoupling *SurfaceCoupling::get(const std::string &Name); ``` -#### 4.2.3 Import and Export +Other portions of the code can inquire whether running in coupled mode by +doing: + +```cpp +if (!SurfaceCoupling::getDefault) { + // no instance of SurfaceCoupling, therefore running in standalone mode +} +``` + +#### 4.2.3 Import, Apply, Accumulate, Export `importFromCoupler` unpacks the driver array into typed member arrays using the name→column-index map. The stride calculation is layout-dependent: MCT @@ -178,18 +189,51 @@ radiation to be positive) are applied inline. The corrections are applied to address numerical issues, not and physical one, in the off chance monotonicity is not preserved during remapping. -`exportToCoupler` packs export arrays into the driver array. State fields -are divided by `NAccumSteps` (interval mean); flux fields are packed directly -(interval total). +`applyImportedState` copies imported fields from the `SurfaceCoupling` class +into the appropriate arrays in the `Forcing` class. This method will be called +directly after `importFromCoupler` is called, but outside of the coupled loop. -`applyImportedState` copies imported fields into the appropriate locations in -`AuxiliaryState` for use by ocean physics. +`accumulateExportState` is called *within* the coupled loop, and adds the +current-step values to running sums each ocean timestep. -`accumulateExportState` adds current-step values to running sums each ocean -timestep. +`exportToCoupler` packs export arrays into the driver array. This function is +called directly after the coupled loop. State fields are divided by +`NAccumSteps` (interval mean); flux fields are packed directly (interval total). `resetAccumulators` zeroes all export arrays and sets `NAccumSteps = 0`. +A rough sketch of how/when these function will be called within `OcnRun` looks +like: + +```cpp +// fetch default OceanState, TimeStepper, and SurfaceCoupler +OceanState *DefOceanState = OceanState::getDefault(); +TimeStepper *DefTimeStepper = TimeStepper::getDefault(); +SurfaceCoupler *DefSurfaceCoupler = SurfaceCoupler::getDefault(); + +// get coupling alarm +Alarm *CouplingAlarm = DefTimeStepper->getCouplingAlarm(); + +// these two could be wrapped into a single call +DefSurfaceCoupler->importFromCoupler(...); +DefSurfaceCoupler->applyImportedState(...); + +while (Err == 0 && !(CouplingAlarm->isRinging())) { + + DefTimeStepper->doStep(DefOceanState, SimTime); + + DefSurfaceCoupler->accumulateExportState(...); + +} + +// if coupler tells us, force write restate stream + +DefSurfaceCoupler->exportToCoupler(...); + +// Reset accumulators +DefSurfaceCoupler->resetAccumulators(...); +``` + #### 4.2.4 Conversion Methods Temperature conversion between conservative (Omega internal) and in situ From e411c3e4d1bb0dc7bff114efd824a9da8da3f839 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Mon, 22 Jun 2026 14:31:16 -0600 Subject: [PATCH 5/6] Rename routines and clarify accumulation --- components/omega/doc/design/Coupling.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md index aab2a317d804..e31782398758 100644 --- a/components/omega/doc/design/Coupling.md +++ b/components/omega/doc/design/Coupling.md @@ -128,8 +128,8 @@ class SurfaceCoupling { // Public methods int importFromCoupler(const Real *x2oData, int NFields, int NCells); int exportToCoupler(Real *o2xData, int NFields, int NCells); - int applyImportedState(AuxiliaryState *AuxState); - int accumulateExportState(const OceanState *State, + int applyImportFields(AuxiliaryState *AuxState); + int accumulateExportFields(const OceanState *State, const Array3DReal &TracerArray, int TimeLevel); void resetAccumulators(); @@ -189,16 +189,19 @@ radiation to be positive) are applied inline. The corrections are applied to address numerical issues, not and physical one, in the off chance monotonicity is not preserved during remapping. -`applyImportedState` copies imported fields from the `SurfaceCoupling` class +`applyImportFields` copies imported fields from the `SurfaceCoupling` class into the appropriate arrays in the `Forcing` class. This method will be called directly after `importFromCoupler` is called, but outside of the coupled loop. -`accumulateExportState` is called *within* the coupled loop, and adds the -current-step values to running sums each ocean timestep. +`accumulateExportFields` is called *within* the coupled loop, and adds the +current-step values to running sums each ocean timestep. The accumulation +step in agnostic of whether the fields are passed back to the coupler as +sums or averages. `exportToCoupler` packs export arrays into the driver array. This function is called directly after the coupled loop. State fields are divided by -`NAccumSteps` (interval mean); flux fields are packed directly (interval total). +`NAccumSteps` (interval mean); flux fields are packed directly (interval total); +and `SSH` is passed as an instantaneous field. `resetAccumulators` zeroes all export arrays and sets `NAccumSteps = 0`. @@ -216,13 +219,13 @@ Alarm *CouplingAlarm = DefTimeStepper->getCouplingAlarm(); // these two could be wrapped into a single call DefSurfaceCoupler->importFromCoupler(...); -DefSurfaceCoupler->applyImportedState(...); +DefSurfaceCoupler->applyImportFields(...); while (Err == 0 && !(CouplingAlarm->isRinging())) { DefTimeStepper->doStep(DefOceanState, SimTime); - DefSurfaceCoupler->accumulateExportState(...); + DefSurfaceCoupler->accumulateExportFields(...); } @@ -280,7 +283,7 @@ To add a new coupling field: 2. Add the field registration in `registerFields()` 3. Add an unpack/pack call in `importFromCoupler`/`exportToCoupler` 4. Add the name/index entry in the Fortran bridge file -5. Fill the field in `applyImportedState()` or `accumulateExportState()` +5. Fill the field in `applyImportFields()` or `accumulateExportFields()` No changes to method signatures or ordering constraints are needed. From b4ba9ceb58c8f2df6c280d2a8425efbc9eb32061 Mon Sep 17 00:00:00 2001 From: Andrew Nolan Date: Thu, 2 Jul 2026 17:09:45 -0700 Subject: [PATCH 6/6] Address review feedback and reconcile design doc with SfcCoupling impl Rename SurfaceCoupling -> SfcCoupling throughout; document split CplToOcnFields/OcnToCplFields containers, Welford running-average accumulation, attachData's unmanaged strided views, and the import/apply/update/export method flow to match the implementation in #458. Document the once-per-interval unit conversion added in #458: device averages stay in native units for correct Welford math, conversion (temperature only; salinity is a TODO) happens once per interval in copyToHost via a local Teos10Eos, and the device arrays are private on OcnToCplFields so external code can't read unconverted data. --- components/omega/doc/design/Coupling.md | 503 +++++++++++++++--------- 1 file changed, 323 insertions(+), 180 deletions(-) diff --git a/components/omega/doc/design/Coupling.md b/components/omega/doc/design/Coupling.md index e31782398758..a4885ce88fb1 100644 --- a/components/omega/doc/design/Coupling.md +++ b/components/omega/doc/design/Coupling.md @@ -3,7 +3,7 @@ ## 1 Overview -The `SurfaceCoupling` class manages all fields exchanged between Omega and +The `SfcCoupling` class manages all fields exchanged between Omega and the coupled system (via MCT or MOAB drivers). It owns import and export coupling field arrays, unpacks driver attribute-vector data into typed Omega arrays, applies imported fields to internal forcing state, @@ -17,8 +17,9 @@ fields (BGC, waves, land-ice). ### 2.1 Requirement: Own coupling field storage -The class must own all coupling fields as `Array1DReal` arrays indexed -over ocean cells, consistent with other Omega field storage. +The class must own all coupling fields as `Array1DReal`/`HostArray1DReal` +arrays indexed over ocean cells, consistent with other Omega field +storage. ### 2.2 Requirement: Clean driver boundary @@ -31,14 +32,22 @@ infrastructure. ### 2.3 Requirement: Field system integration All coupling fields must be registerable with Omega's Field system for -diagnostic output. +diagnostic output. Not yet implemented in the initial pass. ### 2.4 Requirement: Per-coupling-interval operations The class must provide methods to import fields, pass the received import fields to the appropriate forcing class member variables, -accumulate export quantities each ocean timestep, export -accumulated fields, and reset accumulators. +accumulate export quantities each ocean timestep, and export +accumulated fields. Accumulators are reset as part of the export step, +so no separate reset call is required in the run loop. + +`applyImportFields` writes directly into `Forcing` member arrays, the +same arrays populated by file-based input in standalone runs. This +keeps `Forcing` agnostic to whether its data originates from the +coupler or from a file, and `SfcCoupling` itself has no user-facing +runtime configuration options; all configuration comes from the +coupler at initialization (see 4.1.1). ### 2.5 Requirement: Driver architecture agnosticism @@ -54,10 +63,15 @@ per-coupling-interval operations should occur. ### 2.7 Requirement: Coupler conversion layer -Private conversion methods must handle unit conversions and state -variable transformations between Omega's internal representation and -coupler expectations (e.g., conservative temperature ↔ in situ -temperature). +Unit conversions and state variable transformations between Omega's +internal representation and coupler expectations (e.g., conservative +temperature → in situ temperature, absolute → practical salinity) must +be supported, and must be done once per coupling interval rather than +every ocean timestep, since evaluating the EOS polynomial is expensive. +Converted and unconverted representations must not both be reachable +from outside the class, to prevent unit confusion. Temperature +conversion is implemented (4.2.5); practical salinity conversion is a +TODO (see 2.8). ### 2.8 Requirement: Extensible design @@ -67,7 +81,19 @@ interfaces or method signatures should be needed. ## 3 Algorithmic Formulation -No algorithms are required. +Export fields are accumulated on-device each ocean timestep using Welford's +online running-average algorithm, rather than a running sum divided at +export time: +```cpp +KOKKOS_INLINE_FUNCTION Real updateAverage(const Real OldAvg, + const Real NewValue, + const I4 NAccumSteps) { + return OldAvg + (NewValue - OldAvg) / (NAccumSteps + 1); +} +``` +This produces the same interval-mean result as a naive sum-then-divide, but +avoids growing partial sums. Instantaneous fields (e.g. `SSH`) bypass +accumulation and are read directly from their source at export time. ## 4 Design @@ -80,253 +106,370 @@ An enum class specifies the coupled driver layout: enum class CouplingLayout { MCT, MOAB }; ``` -The initial implementation will not have any runtime configuration options. +All initialization inputs are supplied by the coupler at runtime and +bundled into a single struct, rather than passed as separate arguments: +```cpp +struct CouplingInitParams { + int NImportFields; + int NExportFields; + std::map ImportIdx; + std::map ExportIdx; + TimeInterval CouplingTimeStep; + CouplingLayout Layout; +}; +``` +`SfcCoupling` has no user-facing runtime configuration options. #### 4.1.2 Class/structs/data types -`SurfaceCoupling` lives in `src/ocn/` alongside `AuxiliaryState` and +`SfcCoupling` lives in `src/ocn/` alongside `AuxiliaryState` and `OceanState`. It has no direct driver dependencies. The bridge layer in `src/drivers/coupled/` is the only code that calls it. +Import (x2o) and export (o2x) fields are grouped into two small container +classes, rather than flat members directly on `SfcCoupling`. Each is +constructed with a name `Suffix` and `Mesh` so multiple named `SfcCoupling` +instances don't collide on Kokkos view labels: + +```cpp +// x2o: Coupler to Ocean. Host-only; applyImportFields() copies to the +// device arrays owned by Forcing. +class CplToOcnFields { + public: + HostArray1DReal SfcStressZonal; ///< Foxx_taux [N m⁻²] + HostArray1DReal SfcStressMerid; ///< Foxx_tauy [N m⁻²] + + CplToOcnFields(const std::string &Suffix, const HorzMesh *Mesh); +}; + +// o2x: Ocean to Coupler. Averaged fields keep a private device array +// (updated each ocean timestep, native Omega units) plus a public host +// mirror (converted units, packed into the driver buffer). Device +// arrays are private so external code cannot read them in native units +// and mistake them for the (unit-converted) host mirrors. +class OcnToCplFields { + public: + HostArray1DReal AvgSfcTemperatureH; ///< So_t [K], in situ approx + HostArray1DReal AvgSfcSalinityH; ///< So_s [g kg⁻¹], TODO: practical + HostArray1DReal AvgSfcVelocityZonalH; ///< So_u [m s⁻¹] + HostArray1DReal AvgSfcVelocityMeridH; ///< So_v [m s⁻¹] + HostArray1DReal InstSshCellH; ///< So_ssh [m], instantaneous + + void updateFields(const OceanState *State, const Array3DReal &TracerArray, + I4 NAccumSteps, I4 NCellsOwned); + void copyToHost(); + void resetFields(); + + OcnToCplFields(const std::string &Suffix, const HorzMesh *Mesh); + + private: + Array1DReal AvgSfcTemperature; ///< [°C], conservative temperature + Array1DReal AvgSfcSalinity; ///< [g kg⁻¹], absolute salinity + Array1DReal AvgSfcVelocityZonal; + Array1DReal AvgSfcVelocityMerid; + + // Scratch buffer for the in situ/Kelvin conversion in copyToHost() + Array1DReal InSituTempScratch; +}; +``` + +`updateFields` (accumulation) and the conversion in `copyToHost` both +need direct access to the device arrays, so that logic lives on +`OcnToCplFields` itself rather than on `SfcCoupling`; +`SfcCoupling::updateExportFields` is now a thin wrapper that just calls +`OcnToCpl.updateFields(...)` and increments `NAccumSteps`. + +Only the fields above are wired up in the initial implementation. The +remaining fields from the original field list are deferred to follow-on +work and added the same way (4.5): SW/LW heat fluxes, latent/sensible +heat, snow/rain/evaporation, sea-ice and iceberg heat/freshwater fluxes, +`SeaIceSaltFlux` (renamed from `SeaIceSalinityFlux`), river/ice runoff, +ice fraction, `WindSpeed10m` (needed for KPP), sea-ice/atmospheric +pressure, SSH gradients, sea-ice formation heat (to be renamed once +sign-definite, e.g. melt potential), frazil ice mass, and freshwater heat +flux. + ```cpp -class SurfaceCoupling { +class SfcCoupling { public: - // Import fields (x2o): coupler → ocean - Array1DReal SurfaceStressZonal; ///< Foxx_taux [N m⁻²] - Array1DReal SurfaceStressMeridional; ///< Foxx_tauy [N m⁻²] - Array1DReal SWHeatFlux; ///< Foxx_swnet [W m⁻²] - Array1DReal LatentHeatFlux; ///< Foxx_lat [W m⁻²] - Array1DReal SensibleHeatFlux; ///< Foxx_sen [W m⁻²] - Array1DReal LWHeatFluxUp; ///< Foxx_lwup [W m⁻²] - Array1DReal LWHeatFluxDown; ///< Faxa_lwdn [W m⁻²] - Array1DReal SeaIceHeatFlux; ///< Fioi_melth [W m⁻²] - Array1DReal IcebergHeatFlux; ///< Fioi_bergh [W m⁻²] - Array1DReal SnowFlux; ///< Faxa_snow [kg m⁻² s⁻¹] - Array1DReal RainFlux; ///< Faxa_rain [kg m⁻² s⁻¹] - Array1DReal EvaporationFlux; ///< Foxx_evap [kg m⁻² s⁻¹] - Array1DReal SeaIceFreshWaterFlux; ///< Fioi_meltw [kg m⁻² s⁻¹] - Array1DReal IcebergFreshWaterFlux; ///< Fioi_bergw [kg m⁻² s⁻¹] - Array1DReal SeaIceSalinityFlux; ///< Fioi_salt [kg m⁻² s⁻¹] - Array1DReal RiverRunoffFlux; ///< Foxx_rofl [kg m⁻² s⁻¹] - Array1DReal IceRunoffFlux; ///< Foxx_rofi [kg m⁻² s⁻¹] - Array1DReal IceFraction; ///< Si_ifrac [-] - Array1DReal SeaIcePressure; ///< Si_bpress [Pa] - Array1DReal AtmosphericPressure; ///< Sa_pslv [Pa] - - // Export fields (o2x): ocean → coupler - Array1DReal SurfaceTemperature; ///< So_t [°C] - Array1DReal SurfaceSalinity; ///< So_s [g kg⁻¹] - Array1DReal SurfaceVelocityZonal; ///< So_u [m s⁻¹] - Array1DReal SurfaceVelocityMeridional; ///< So_v [m s⁻¹] - Array1DReal SSH; ///< So_ssh [m] - Array1DReal SSHGradientZonal; ///< So_dhdx [m m⁻¹] - Array1DReal SSHGradientMeridional; ///< So_dhdy [m m⁻¹] - Array1DReal SeaIceFormationHeat; ///< Fioo_q [W m⁻²] - Array1DReal FrazilIceMass; ///< Fioo_frazil [kg m⁻² s⁻¹] - Array1DReal FreshWaterHeatFlux; ///< Faoo_h2otemp[W m⁻²] - - // Public methods - int importFromCoupler(const Real *x2oData, int NFields, int NCells); - int exportToCoupler(Real *o2xData, int NFields, int NCells); - int applyImportFields(AuxiliaryState *AuxState); - int accumulateExportFields(const OceanState *State, - const Array3DReal &TracerArray, - int TimeLevel); - void resetAccumulators(); + std::string Name; + I4 NCellsOwned; ///< Number of cells owned by this task + I4 NImportFields; ///< Num of fields in the x2o pointer array + I4 NExportFields; ///< Num of fields in the o2x pointer array + + CplToOcnFields CplToOcn; ///< Coupler to Ocean (x2o) + OcnToCplFields OcnToCpl; ///< Ocean to Coupler (o2x) + Alarm CouplingAlarm; ///< Alarm for the coupling interval + + // Unmanaged, strided views over the driver's raw x2o/o2x buffers + Kokkos::View> + CplToOcnView; + Kokkos::View> + OcnToCplView; + + static SfcCoupling *create(const std::string &Name, const HorzMesh *Mesh, + int NImportFields, int NExportFields, + const std::map &ImportIdx, + const std::map &ExportIdx, + TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, + const CouplingLayout &Layout); + static int init(const CouplingInitParams &Params); + ~SfcCoupling(); + static void clear(); + static void erase(const std::string Name); + static SfcCoupling *getDefault(); + static SfcCoupling *get(const std::string Name); + I4 getNAccumSteps() const; + + void attachData(const Real *CplToOcnData, Real *OcnToCplData); + void importFromCoupler(); + void exportToCoupler(); + void applyImportFields(Forcing *Forcing); + void updateExportFields(const OceanState *State, + const Array3DReal &TracerArray); private: - int NAccumSteps = 0; - Alarm CouplingAlarm; + I4 NAccumSteps = 0; + CouplingLayout Layout; std::map ImportIdx; std::map ExportIdx; - CouplingLayout DataLayout; - static Real potTempToConservTemp(Real PotTemp, Real Salinity, Real Pressure); - static Real conservTempToPotTemp(Real ConsTemp, Real Salinity, Real Pressure); + template auto ownedSubView(const View &V) const; }; ``` -### 4.2 Methods +Temperature conversion (2.7) is implemented in `copyToHost` (4.2.5); +practical-salinity conversion is a TODO, so `AvgSfcSalinityH` is still +absolute salinity. -#### 4.2.1 Creation +### 4.2 Methods -The `create` method allocates import/export arrays sized to `NCellsOwned`, stores -the field index maps and `DataLayout`, creates the coupling `Alarm`, and calls -`registerFields`. After creation, `create` internally calls -`accumulateExportState` once to populate the initial export state. +#### 4.2.1 Creation and Initialization +`init` retrieves the default `HorzMesh` and `TimeStepper`, validates that +the coupling interval is not shorter than, and is evenly divisible by, the +ocean timestep (aborting otherwise), then calls `create` to build the +default instance: ```cpp -SurfaceCoupling *SurfaceCoupling::create( - const std::string &Name, int OcnId, const HorzMesh *Mesh, - const std::map &ImportIdx, - const std::map &ExportIdx, - const TimeInterval &CplInterval, CouplingLayout Layout); +static int SfcCoupling::init(const CouplingInitParams &Params); +``` +`create` allocates the `CplToOcn`/`OcnToCpl` field containers sized to +`NCellsOwned`, stores the index maps and `Layout`, and constructs +`CouplingAlarm` on `Stepper`'s `Clock`: +```cpp +static SfcCoupling *SfcCoupling::create( + const std::string &Name, const HorzMesh *Mesh, int NImportFields, + int NExportFields, const std::map &ImportIdx, + const std::map &ExportIdx, TimeStepper *Stepper, + const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout); ``` #### 4.2.2 Retrieval ```cpp -SurfaceCoupling *SurfaceCoupling::getDefault(); -SurfaceCoupling *SurfaceCoupling::get(const std::string &Name); +SfcCoupling *SfcCoupling::getDefault(); +SfcCoupling *SfcCoupling::get(const std::string &Name); ``` -Other portions of the code can inquire whether running in coupled mode by -doing: +Other portions of the code can inquire whether it is running in coupled +mode by doing: ```cpp -if (!SurfaceCoupling::getDefault) { - // no instance of SurfaceCoupling, therefore running in standalone mode +if (!SfcCoupling::getDefault()) { + // no instance of SfcCoupling, therefore running in standalone mode } ``` -#### 4.2.3 Import, Apply, Accumulate, Export - -`importFromCoupler` unpacks the driver array into typed member arrays using -the name→column-index map. The stride calculation is layout-dependent: MCT -uses `col*NCells + i`, MOAB uses `i*NFields + col`. Unit conversions (e.g., -in situ → conservative temperature) and corrections (e.g. forcing shortwave -radiation to be positive) are applied inline. The corrections are applied to -address numerical issues, not and physical one, in the off chance monotonicity -is not preserved during remapping. +#### 4.2.3 Attaching coupler data -`applyImportFields` copies imported fields from the `SurfaceCoupling` class -into the appropriate arrays in the `Forcing` class. This method will be called -directly after `importFromCoupler` is called, but outside of the coupled loop. - -`accumulateExportFields` is called *within* the coupled loop, and adds the -current-step values to running sums each ocean timestep. The accumulation -step in agnostic of whether the fields are passed back to the coupler as -sums or averages. - -`exportToCoupler` packs export arrays into the driver array. This function is -called directly after the coupled loop. State fields are divided by -`NAccumSteps` (interval mean); flux fields are packed directly (interval total); -and `SSH` is passed as an instantaneous field. - -`resetAccumulators` zeroes all export arrays and sets `NAccumSteps = 0`. +Before import/export, `attachData` wraps the driver's raw `x2o`/`o2x` +pointers in unmanaged, layout-strided Kokkos views, computing the +layout-dependent stride once rather than on every element access: MCT +lays fields out as `(NCellsOwned, NImportFields)` (field index strides +fastest), MOAB as `(NImportFields, NCellsOwned)` (cell index strides +fastest). +```cpp +void SfcCoupling::attachData(const Real *CplToOcnData, Real *OcnToCplData); +``` -A rough sketch of how/when these function will be called within `OcnRun` looks -like: +#### 4.2.4 Import, Apply, Update, Export + +`importFromCoupler` looks up each field's column index from `ImportIdx` +and copies it out of `CplToOcnView` into the corresponding `CplToOcn` +array. + +`applyImportFields` deep-copies `CplToOcn` arrays into the matching +`Forcing` arrays (e.g. `Forcing->SfcStressForcing.ZonalStressCell`), +restricted to the owned-cell subview since `SfcCoupling` has no halo +information; `Forcing` is responsible for the halo exchange. Called once +per coupling interval, directly after `importFromCoupler`, outside the +ocean timestep loop. + +`updateExportFields` is called *within* the ocean timestep loop. It +updates each `OcnToCpl` running average in place using Welford's +algorithm (3), then increments `NAccumSteps`. Velocity currently uses a +placeholder constant (`1e-4`) pending vector reconstruction, to avoid +producing zero (and therefore divide-by-zero/infinite flux) velocities +downstream in the coupler; this is a known limitation to revisit before +production use. + +`exportToCoupler` calls `OcnToCpl.copyToHost()`, which converts and +copies device arrays to their host mirrors (SSH is read directly from +`VertCoord`'s host SSH array, since it is instantaneous and not +accumulated), then packs the host mirrors into `OcnToCplView` at their +export indices, then resets `OcnToCpl` and `NAccumSteps` for the next +interval. Resetting is folded into `exportToCoupler`; there is no +separate `resetAccumulators` call in the run loop. + +A rough sketch of how/when these functions are called within `OcnRun`: ```cpp -// fetch default OceanState, TimeStepper, and SurfaceCoupler +// fetch default OceanState, TimeStepper, and SfcCoupling OceanState *DefOceanState = OceanState::getDefault(); TimeStepper *DefTimeStepper = TimeStepper::getDefault(); -SurfaceCoupler *DefSurfaceCoupler = SurfaceCoupler::getDefault(); +SfcCoupling *DefSfcCoupling = SfcCoupling::getDefault(); -// get coupling alarm -Alarm *CouplingAlarm = DefTimeStepper->getCouplingAlarm(); +DefSfcCoupling->attachData(CplToOcnData, OcnToCplData); // these two could be wrapped into a single call -DefSurfaceCoupler->importFromCoupler(...); -DefSurfaceCoupler->applyImportFields(...); +DefSfcCoupling->importFromCoupler(); +DefSfcCoupling->applyImportFields(DefForcing); -while (Err == 0 && !(CouplingAlarm->isRinging())) { +while (Err == 0 && !(DefSfcCoupling->CouplingAlarm.isRinging())) { DefTimeStepper->doStep(DefOceanState, SimTime); - DefSurfaceCoupler->accumulateExportFields(...); + DefSfcCoupling->updateExportFields(DefOceanState, TracerArray); } -// if coupler tells us, force write restate stream +// if coupler tells us, force write restart stream -DefSurfaceCoupler->exportToCoupler(...); - -// Reset accumulators -DefSurfaceCoupler->resetAccumulators(...); +DefSfcCoupling->exportToCoupler(); // also resets accumulators ``` -#### 4.2.4 Conversion Methods - -Temperature conversion between conservative (Omega internal) and in situ -(coupler expectation) is handled as private static methods. Similar handling -will occur for salinity with conversion between absolute (Omega internal) and -practical (MPAS-Seaice expectation). - -#### 4.2.5 Destruction and removal +#### 4.2.5 Conversion Methods + +Conversion happens in `copyToHost`, once per coupling interval rather +than every ocean timestep, since the EOS conversion polynomial is +expensive. Conservative temperature is converted to potential +temperature via a local `Teos10Eos` instance's `calcPtFromCt` (only +under `EosType::Teos10Eos`; otherwise passed through unconverted), then +shifted to Kelvin (`+ TkFrz`) as an in situ approximation. The result is +written to a private scratch buffer (`InSituTempScratch`) rather than +in place, so the device `AvgSfcTemperature` array driving the running +average (3) always stays in native (deg C, conservative) units. A +local `Teos10Eos` is constructed rather than reusing `Eos::getInstance()` +since `calcPtFromCt` needs no config-derived state, avoiding exposure of +`Eos` internals. Salinity conversion (absolute → practical) is a TODO; +`AvgSfcSalinityH` is currently absolute salinity, copied through +unconverted. + +The device arrays backing the conversion (`AvgSfcTemperature`, +`AvgSfcSalinity`, ...) are private members of `OcnToCplFields`; only the +post-conversion host mirrors are public. This prevents external code +from reading the native-unit device arrays and confusing them with the +converted host mirrors -- a hazard motivated by `copyToHost` itself +needing to reach into `VertCoord::SshCell` (a raw device array) to +populate `InstSshCellH`, which showed how easy it is to mix up +device/host and native/converted units without this guard. + +#### 4.2.6 Destruction and removal ```cpp -void SurfaceCoupling::erase(const std::string &Name); -void SurfaceCoupling::clear(); +static void SfcCoupling::erase(const std::string Name); +static void SfcCoupling::clear(); ``` +The destructor does not detach `CouplingAlarm` from the shared `Clock`; +callers must ensure `SfcCoupling` instances outlive the `Clock`, consistent +with other `Alarm` owners in Omega. ### 4.3 Driver Interface Bridge -The bridge layer in `src/drivers/coupled/` provides `extern "C"` entry points -called from the Fortran driver (`ocn_comp_mct.F90`). The coupling interval -and ocean timestep are independent: the driver calls `ocn_run_mct` once per -coupling interval; the ocean *can* take multiple timesteps within the coupling -interval. +The bridge layer in `src/drivers/coupled/` is not yet implemented; it will +provide `extern "C"` entry points, called from the Fortran driver +(`ocn_comp_mct.F90`), that build a `CouplingInitParams` and call +`SfcCoupling::init`. The coupling interval and ocean timestep are +independent: the driver calls `ocn_run_mct` once per coupling interval; +the ocean can take multiple timesteps within it. #### 4.3.1 Name-based Field Index Mapping -At startup, the Fortran bridge populates parallel arrays of Omega field names -and driver column indices, then passes them to `omega_ocn_init`. C++ builds -`std::map` objects for import/export lookups. This approach -is driver-agnostic: MCT and MOAB differ only in how column indices are -obtained. Field names are passed as fixed-length null-terminated character -arrays (standard Fortran-C interop), parsed by stepping through the flat -array at stride `FieldNameLen`. +At startup, the Fortran bridge will populate parallel arrays of Omega +field names and driver column indices and pass them into +`CouplingInitParams::ImportIdx`/`ExportIdx`. This approach is +driver-agnostic: MCT and MOAB differ only in how column indices are +obtained. ### 4.4 Interval Accumulation -Export state fields are accumulated each ocean timestep and divided by -`NAccumSteps` at export time (interval mean). Export flux fields are -accumulated and packed directly without dividing (interval total). -`resetAccumulators` is called by `omega_ocn_export` after packing. +Export fields accumulate a running average each ocean timestep via +Welford's algorithm (3), in native Omega units; `SSH` is read directly +from its source at export time instead of being accumulated. Unit +conversion (4.2.5) is deferred to `copyToHost`/`exportToCoupler`, so it +runs once per interval rather than once per accumulation step. +`exportToCoupler` resets all accumulators after packing, so no separate +reset call is needed in the run loop. Interval summation (as opposed to +averaging), needed for flux-like fields, is not yet implemented; it can +reuse `NAccumSteps` (average x steps) or a dedicated accumulation path +once flux fields are added. ### 4.5 Extensibility To add a new coupling field: -1. Add an `Array1DReal` member to `SurfaceCoupling.h` -2. Add the field registration in `registerFields()` -3. Add an unpack/pack call in `importFromCoupler`/`exportToCoupler` -4. Add the name/index entry in the Fortran bridge file -5. Fill the field in `applyImportFields()` or `accumulateExportFields()` +1. Add an `Array1DReal`/`HostArray1DReal` member to `CplToOcnFields` or + `OcnToCplFields` in `SfcCoupling.h`, and initialize it in the + corresponding constructor +2. Add an unpack/pack call in `importFromCoupler`/`exportToCoupler` (and, + for export fields, an update in `updateExportFields`) +3. Add the name/index entry in the Fortran bridge file +4. Fill the field in `applyImportFields()` or `updateExportFields()` No changes to method signatures or ordering constraints are needed. ## 5 Verification and Testing -### 5.1 Test: Import/export round-trip - -Create synthetic coupling arrays with known values, call -`importFromCoupler`, verify member arrays match expected values. Pack -export arrays and verify the output matches. Test both MCT and MOAB -layouts. - - tests requirement 2.1, 2.2 - -### 5.2 Test: Interval averaging for state fields +### 5.1 Test: Import round-trip -Accumulate the same state values over multiple simulated timesteps, -export, and verify the output equals the mean. - - tests requirement 2.4 +Attach synthetic `x2o` buffers with known, cell-varying values, call +`importFromCoupler`, and verify the `CplToOcn` arrays match expected +values. Test both MCT and MOAB layouts. + - tests requirement 2.1, 2.2, 2.5 -### 5.3 Test: Interval summation for flux fields +### 5.2 Test: Apply imported fields to Forcing -Accumulate flux values over multiple timesteps, export, and verify the -output equals the total sum (not divided). - - tests requirement 2.4 +Populate `CplToOcn` arrays directly, call `applyImportFields`, and verify +the owned-cell subview of the matching `Forcing` arrays matches. + - tests requirement 2.1, 2.4 -### 5.4 Test: Name-keyed field mapping +### 5.3 Test: Running-average accumulation and conversion -Build the name→index map with different fill orders and verify -import/export produces correct results regardless of order. Request an -unknown field name and verify error logging. - - tests requirement 2.2, 2.8 +Advance the model clock through a known number of ocean timesteps within +one coupling interval, calling `updateExportFields` with cell- and +step-varying tracer values each timestep (using the coupling `Alarm` to +end the loop, as in the sketch in 4.2.4). Verify `NAccumSteps` matches +the step count and, after `copyToHost` (the only sanctioned way to read +the averages), the resulting host-mirror averages match the analytic +mean within round-off tolerance. `EosType` is forced to `constant` for +the test suite so `calcPtFromCt` is an identity, isolating the `+TkFrz` +Kelvin shift; salinity is checked unconverted (practical conversion is a +TODO). + - tests requirement 2.4, 2.6, 2.7 -### 5.5 Test: Coupling alarm +### 5.4 Test: Export round-trip and reset -Create a `SurfaceCoupling` with a known coupling interval. Advance the -simulation clock and verify the coupling alarm rings at the expected -interval boundaries and does not ring between them. - - tests requirement 2.6 +Populate `OcnToCpl` averages via `updateExportFields` (seeded to the +target value using `NAccumSteps == 0`), call `exportToCoupler`, and +verify the packed `o2x` buffer matches expected values (temperature +offset by `TkFrz`) for both MCT and MOAB layouts. Also verify `OcnToCpl` +host mirrors and `NAccumSteps` are zeroed afterward. + - tests requirement 2.1, 2.4, 2.5, 2.7 -### 5.6 Test: Coupling loop integration +### 5.5 Test: Object lifecycle -Simulate a complete coupling interval: import, run multiple ocean -timesteps with accumulation, export, and reset. Verify the full -import → accumulate → export → reset cycle produces correct results and -that the accumulators are properly zeroed for the next interval. - - tests requirement 2.4, 2.6 +Create a non-default, named `SfcCoupling`, verify it can be retrieved +with `get`, erase it, and verify retrieval afterward fails. + - tests requirement 2.8