Surface Coupling
1 Overview
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,
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/HostArray1DReal
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. 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, 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
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
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
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
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:
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
4.1 Data types and parameters
4.1.1 Parameters
An enum class specifies the coupled driver layout:
enum class CouplingLayout { MCT, MOAB };
All initialization inputs are supplied by the coupler at runtime and bundled into a single struct, rather than passed as separate arguments:
struct CouplingInitParams {
int NImportFields;
int NExportFields;
std::map<std::string, int> ImportIdx;
std::map<std::string, int> ExportIdx;
TimeInterval CouplingTimeStep;
CouplingLayout Layout;
};
SfcCoupling has no user-facing runtime configuration options.
4.1.2 Class/structs/data types
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:
// 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.
class SfcCoupling {
public:
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<const Real **, Kokkos::LayoutStride, Kokkos::HostSpace,
Kokkos::MemoryTraits<Kokkos::Unmanaged>>
CplToOcnView;
Kokkos::View<Real **, Kokkos::LayoutStride, Kokkos::HostSpace,
Kokkos::MemoryTraits<Kokkos::Unmanaged>>
OcnToCplView;
static SfcCoupling *create(const std::string &Name, const HorzMesh *Mesh,
int NImportFields, int NExportFields,
const std::map<std::string, int> &ImportIdx,
const std::map<std::string, int> &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:
I4 NAccumSteps = 0;
CouplingLayout Layout;
std::map<std::string, int> ImportIdx;
std::map<std::string, int> ExportIdx;
template <class View> auto ownedSubView(const View &V) const;
};
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 Methods
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:
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:
static SfcCoupling *SfcCoupling::create(
const std::string &Name, const HorzMesh *Mesh, int NImportFields,
int NExportFields, const std::map<std::string, int> &ImportIdx,
const std::map<std::string, int> &ExportIdx, TimeStepper *Stepper,
const TimeInterval &CouplingTimeStep, const CouplingLayout &Layout);
4.2.2 Retrieval
SfcCoupling *SfcCoupling::getDefault();
SfcCoupling *SfcCoupling::get(const std::string &Name);
Other portions of the code can inquire whether it is running in coupled mode by doing:
if (!SfcCoupling::getDefault()) {
// no instance of SfcCoupling, therefore running in standalone mode
}
4.2.3 Attaching coupler data
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).
void SfcCoupling::attachData(const Real *CplToOcnData, Real *OcnToCplData);
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:
// fetch default OceanState, TimeStepper, and SfcCoupling
OceanState *DefOceanState = OceanState::getDefault();
TimeStepper *DefTimeStepper = TimeStepper::getDefault();
SfcCoupling *DefSfcCoupling = SfcCoupling::getDefault();
DefSfcCoupling->attachData(CplToOcnData, OcnToCplData);
// these two could be wrapped into a single call
DefSfcCoupling->importFromCoupler();
DefSfcCoupling->applyImportFields(DefForcing);
while (Err == 0 && !(DefSfcCoupling->CouplingAlarm.isRinging())) {
DefTimeStepper->doStep(DefOceanState, SimTime);
DefSfcCoupling->updateExportFields(DefOceanState, TracerArray);
}
// if coupler tells us, force write restart stream
DefSfcCoupling->exportToCoupler(); // also resets accumulators
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
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/ 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 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 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:
Add an
Array1DReal/HostArray1DRealmember toCplToOcnFieldsorOcnToCplFieldsinSfcCoupling.h, and initialize it in the corresponding constructorAdd an unpack/pack call in
importFromCoupler/exportToCoupler(and, for export fields, an update inupdateExportFields)Add the name/index entry in the Fortran bridge file
Fill the field in
applyImportFields()orupdateExportFields()
No changes to method signatures or ordering constraints are needed.
5 Verification and Testing
5.1 Test: Import round-trip
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.2 Test: Apply imported fields to Forcing
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.3 Test: Running-average accumulation and conversion
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.4 Test: Export round-trip and reset
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.5 Test: Object lifecycle
Create a non-default, named SfcCoupling, verify it can be retrieved
with get, erase it, and verify retrieval afterward fails.
tests requirement 2.8