Exchange (intersection) grid
When the coupled simulation uses an Oceananigans ocean (the CMIP configuration), ClimaCoupler can compute atmosphere-ocean and atmosphere-ice turbulent fluxes on the exchange grid: the set of polygons formed where the elements of the ClimaCore cubed-sphere spectral-element (SE) boundary space intersect the finite-volume (FV) cells of the ocean's TripolarGrid. This is enabled by default and controlled by the use_intersection_grid configuration option.
Why
Computing fluxes on the boundary (atmosphere) space and remapping them to the ocean loses coastline detail: the ocean's land-sea mask (its immersed bathymetry) does not coincide with the coupler's land fraction, so flux weights and the region where the ocean actually has wet cells disagree (issue #1838). The exchange grid solves both problems at once, because the same intersection areas define
- the surface area fractions, and
- the weights with which per-polygon fluxes are aggregated to both sides.
Setting the exchange grid as a FV space also bypasses the need to define a conservative SE to SE regridding; only SE to FV and FV to SE implementations of regrid! (which are already implemented in ConservativeRegridding.jl) are needed. This allows the land and atmosphere to be defined on different boundary spaces at no extra cost.
Geometry and weights
For a more in-depth derivation of the regridding operations, see the ConservativeRegridding.jl documentation. build_exchange_grid constructs the polygons once on the CPU (in Float64) via ConservativeRegridding.jl's intersection-operator API, with polygons over dry (immersed) cells dropped. For each polygon $\\Omega_k$ in SE element $e$, the SEM basis integrals $B_{kn} = \\int_{\\Omega_k} \\phi_n \\, dA$ provide:
- a gather (SE nodes → FV polygons): $\\bar f_k = \\sum_n (B_{kn}/\\sum_n B_{kn}) f_n$ — rows sum to 1, constants preserved exactly;
- a scatter (FV polygons → SE nodes): the per-element L2 projection $F_n = \\sum_k (B_{kn}/(Jw)_n) F_k$, followed by
weighted_dss!; - wet and total nodal coverage,
node_covandnode_cov_total. At each SE node,node_covis how much nearby area lies over wet ocean, andnode_cov_totalis how much lies over any ocean cell (wet or dry). The wet-ocean fraction is their rationode_cov / node_cov_total(clamped to $[0, 1]$). Using the ratio rather thannode_covalone corrects for projection and geometry errors that affect both equally.
On the FV side, each polygon belongs to exactly one cell; gathers are direct reads and scatters are area-weighted averages.
Surface fractions
Each coupling step, FieldExchanger.align_surface_fractions! composes
ice = clamp(ice_concentration, 0, wet)
ocean = wet - ice
land = 1 - wetwhich sum to 1 identically (asserted at runtime)
Per-polygon fluxes
For the ocean and for sea ice, FluxCalculator.compute_surface_fluxes! runs one SurfaceFluxes.surface_fluxes evaluation per polygon (the ice version includes the skin-temperature diagnosis; ice-free polygons short-circuit). Atmospheric inputs are gathered from the SE nodes with velocities converted to the global UV (east/north) basis; surface inputs (SST, sea-ice concentration, ice thickness, interface temperature) are read directly from the owning FV cell.
Aggregation:
- To the ocean/ice: the sea-ice-concentration weighting is applied per polygon —
(1 - sic)for ocean heat/salinity/momentum, per-ice-area fluxes for ClimaSeaIce — before the conservative area-weighted scatter, so the ice/ocean partition is resolved at polygon level rather than after remapping. - To the atmosphere: the raw (coverage-weighted) L2 scatter is DSS'd in the UV basis and divided by the DSS'd weighted coverage — ocean fluxes are
(1 - sic)-weighted and ice fluxessic-weighted averages, consistent with the open-ocean and ice area fractionsupdate_flux_fields!multiplies them by. Momentum is converted back to the local CT1/CT2 components only at the end (the UV basis is global, so its components are DSS-safe scalars).
Slow surfaces time-average on per-polygon accumulators (FluxCalculator.push_and_reset! overrides), so the averaged flux pushed to the ocean/ice retains full polygon resolution.
Fallbacks
The exchange grid requires a process-local SpectralElementSpace2D boundary space; column (PointSpace) and distributed setups automatically fall back to the regridder-based boundary-space flux path and the legacy fraction update, as does use_intersection_grid: false.
API
ClimaCoupler.FieldExchanger.align_surface_fractions! — FunctionFieldExchanger.align_surface_fractions!(ocean_sim::OceananigansSimulation,
cs::Interfacer.CoupledSimulation) -> BoolOcean-bathymetry-authoritative surface fractions on the exchange grid.
The static wet-ocean fraction (remapping.wet_ocean_fraction, derived from the intersection areas with the ocean's immersed wet mask and DSS'd onto the boundary space) partitions each boundary node into wet and land parts. Sea ice and open ocean subdivide the wet part; land fills the remainder:
ice = clamp(ice_concentration, 0, wet)
ocean = wet - ice
land = 1 - wetso the three fractions sum to 1 identically and the flux weights are, by construction, consistent with where the ocean model actually has wet cells (issue #1838).
Returns false (falling back to the legacy ETOPO-based update) when the exchange grid is not active.
align_surface_fractions!(ocean_sim, cs::Interfacer.CoupledSimulation) -> BoolGive the ocean model the opportunity to set all surface area fractions from its own representation of the land/sea distribution (e.g. a fraction derived from the ocean bathymetry via an exchange grid).
Return true if the fractions were updated (in which case the land-fraction-based update in update_surface_fractions! is skipped), false otherwise. The default implementation returns false; ocean models can extend this method.
Implementations must maintain the invariant that the land, ice, and ocean area fractions sum to 1 at every point of the boundary space.