Skip to content

Commit aa3d870

Browse files
committed
initial ClimaSeaIce infrastructure changes
1 parent 9c97640 commit aa3d870

File tree

9 files changed

+188
-143
lines changed

9 files changed

+188
-143
lines changed

docs/src/fieldexchanger.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The specific fields that are exchanged depend on the requirements of the compone
1212
The fields imported from the atmosphere to the coupler are specified by extending `FieldExchanger.import_atmos_fields!`
1313
The default `import_atmos_fields!` imports radiative fluxes, liquid precipitation, and snow precipitation.
1414

15-
The fields of a component model that get updated by the coupler are specified by extending `FieldExchanger.update_sim!`
15+
The fields of a component model that get updated by the coupler are specified by extending `FieldExchanger.update_sim!`.
1616
The default `update_sim!` for an atmosphere model updates the direct and diffuse surface albedos,
1717
the surface temperature, and the turbulent fluxes.
1818
The default `update_sim!` for a surface model updates the air density, radiative fluxes,
@@ -21,18 +21,24 @@ These updates are done via the `update_field!` function, which must be extended
2121
particular variable and component model.
2222
If an `update_field!` function is not defined for a particular component model, it will be ignored.
2323

24+
Note that turbulent fluxes are not updated in `update_sim!`, but rather via
25+
`FluxCalculator.update_turbulent_fluxes!`, where fluxes are computed between
26+
the atmosphere and each surface model.
27+
2428
## FieldExchanger API
2529

2630
```@docs
31+
ClimaCoupler.FieldExchanger.exchange!
2732
ClimaCoupler.FieldExchanger.update_sim!
2833
ClimaCoupler.FieldExchanger.step_model_sims!
2934
ClimaCoupler.FieldExchanger.update_surface_fractions!
30-
ClimaCoupler.FieldExchanger.exchange!
3135
ClimaCoupler.FieldExchanger.set_caches!
3236
```
3337

3438
## FieldExchanger Internal Functions
3539

3640
```@docs
3741
ClimaCoupler.FieldExchanger.combine_surfaces!
42+
ClimaCoupler.FieldExchanger.resolve_ocean_ice_fractions!
43+
ClimaCoupler.FieldExchanger.import_atmos_fields!
3844
```

docs/src/fluxcalculator.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,23 @@ turbulent fluxes and ancillary quantities, such as the Obukhov length, using
1111
function is called at the end of each coupling step.
1212

1313
All the quantities computed in `turbulent_fluxes!` are calculated
14-
separately for each surface type using the
14+
separately for each surface model using the
1515
[`FluxCalculator.compute_surface_fluxes!`](@ref) function. This function can be
1616
extended by component models if they need specific type of flux calculation, and
1717
a default is provided for models that can use the standard flux calculation.
18-
The final result of `turbulent_fluxes!` is an area-weighted sum of
19-
all the contributions of the various surfaces.
2018

2119
The default method of [`FluxCalculator.compute_surface_fluxes!`](@ref), in turn,
2220
calls [`FluxCalculator.get_surface_fluxes`](@ref). This function uses a thermal
2321
state obtained by using the model surface temperature, extrapolates atmospheric
2422
density adiabatically to the surface, and with the surface humidity (if
2523
available, if not, assuming a saturation specific humidity for liquid phase).
26-
`compute_surface_fluxes!` also updates the component internal fluxes fields with
24+
`compute_surface_fluxes!` also updates the component internal fluxes fields via
2725
[`FluxCalculator.update_turbulent_fluxes!`](@ref), and adds the area-weighted
2826
contribution from this component model to the `CoupledSimulation` fluxes fields.
29-
Any extension of this function for a particular surface model is also expected
30-
to update both the component models' internal fluxes and the CoupledSimulation
31-
object's fluxes fields.
27+
28+
Any extension of `FluxCalculator.compute_surface_fluxes!` for a particular
29+
surface model is also expected to update both the component models' internal
30+
fluxes and the CoupledSimulation object's fluxes fields.
3231

3332
[`FluxCalculator.compute_surface_fluxes!`](@ref) sets:
3433
- the flux of momenta, `F_turb_ρτxz`, `F_turb_ρτyz`;

docs/src/interfacer.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,26 @@ for the following properties:
225225
| `surface_direct albedo` | bulk direct surface albedo | |
226226
| `surface_diffuse albedo` | bulk diffuse surface albedo | |
227227
| `surface_temperature` | surface temperature | K |
228+
| `ice_concentration` | sea ice concentration (*sea ice models only*) | |
228229

229230
!!! note
230231
`area_fraction` is expected to be defined on the boundary space of the simulation,
231232
while all other fields will likely be on the simulation's own space.
232233

234+
!!! note "Sea ice concentration vs. area fraction"
235+
Sea ice models are expected to provide both `area_fraction` and `ice_concentration`.
236+
This may seem redundant, but there are subtle differences between the two.
237+
`ice_concentration` is internal to the ice model and may be determined
238+
via a prognostic variable, prescribed data, etc. `area_fraction` is defined
239+
at the coupler level and may follow some constraints that don't apply to `ice_concentration`.
240+
For example, we require that surface model area fractions sum to 1 at all points;
241+
this constraint is enforced for `area_fraction`, but not for `ice_concentration`.
242+
Additionally, since `area_fraction` is a coupler-defined concept, it is defined on
243+
the coupler boundary space, whereas `ice_concentration` exists on the model's space.
244+
Generally, `ice_concentration` and `area_fraction` should largely agree,
245+
with differences only arising from `area_fraction` corrections and
246+
the fields existing on different spaces.
247+
233248
- `update_field!(::SurfaceModelSimulation, ::Val{property}, field)`:
234249
A function to update the value of property in the component model
235250
simulation, using the values in `field` passed from the coupler
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
to_node(pt::CC.Geometry.LatLongPoint)
3+
4+
Transform `LatLongPoint` into a tuple (long, lat, 0), where the 0 is needed because we only
5+
care about the surface.
6+
"""
7+
@inline to_node(pt::CC.Geometry.LatLongPoint) = pt.long, pt.lat, zero(pt.lat)
8+
# This next one is needed if we have "LevelGrid"
9+
@inline to_node(pt::CC.Geometry.LatLongZPoint) = pt.long, pt.lat, zero(pt.lat)
10+
11+
"""
12+
map_interpolate(points, oc_field::OC.Field)
13+
14+
Interpolate the given 3D field onto the target points.
15+
16+
If the underlying grid does not contain a given point, return 0 instead.
17+
18+
Note: `map_interpolate` does not support interpolation from `Field`s defined on
19+
`OrthogononalSphericalShellGrids` such as the `TripolarGrid`.
20+
21+
TODO: Use a non-allocating version of this function (simply replace `map` with `map!`)
22+
"""
23+
function map_interpolate(points, oc_field::OC.Field)
24+
loc = map(L -> L(), OC.Fields.location(oc_field))
25+
grid = oc_field.grid
26+
data = oc_field.data
27+
28+
# TODO: There has to be a better way
29+
min_lat, max_lat = extrema(OC.φnodes(grid, OC.Center(), OC.Center(), OC.Center()))
30+
31+
map(points) do pt
32+
FT = eltype(pt)
33+
34+
# The oceananigans grid does not cover the entire globe, so we should not
35+
# interpolate outside of its latitude bounds. Instead we return 0
36+
min_lat < pt.lat < max_lat || return FT(0)
37+
38+
fᵢ = OC.Fields.interpolate(to_node(pt), data, loc, grid)
39+
convert(FT, fᵢ)::FT
40+
end
41+
end
42+
43+
"""
44+
surface_flux(f::OC.AbstractField)
45+
46+
Extract the top boundary conditions for the given field.
47+
"""
48+
function surface_flux(f::OC.AbstractField)
49+
top_bc = f.boundary_conditions.top
50+
if top_bc isa OC.BoundaryCondition{<:OC.BoundaryConditions.Flux}
51+
return top_bc.condition
52+
else
53+
return nothing
54+
end
55+
end
56+
57+
function Interfacer.remap(field::OC.Field, target_space)
58+
return map_interpolate(CC.Fields.coordinate_field(target_space), field)
59+
end
60+
61+
function Interfacer.remap(operation::OC.AbstractOperations.AbstractOperation, target_space)
62+
evaluated_field = OC.Field(operation)
63+
OC.compute!(evaluated_field)
64+
return Interfacer.remap(evaluated_field, target_space)
65+
end
66+
67+
"""
68+
set_from_extrinsic_vector!(vector, grid, u_cc, v_cc)
69+
70+
Given the extrinsic vector components `u_cc` and `v_cc` as `Center, Center`
71+
fields, rotate them onto the target grid and remap to `Face, Center` and
72+
`Center, Face` fields, respectively.
73+
"""
74+
function set_from_extrinsic_vector!(vector, grid, u_cc, v_cc)
75+
arch = OC.Architectures.architecture(grid)
76+
77+
# Rotate vector components onto the grid
78+
OC.Utils.launch!(arch, grid, :xy, _rotate_vector!, u_cc, v_cc, grid)
79+
80+
# Fill halo regions with the rotated vector components so we can use them to interpolate
81+
OC.fill_halo_regions!(u_cc)
82+
OC.fill_halo_regions!(v_cc)
83+
84+
# Interpolate the vector components to face/center and center/face respectively
85+
OC.Utils.launch!(
86+
arch,
87+
grid,
88+
:xy,
89+
_interpolate_vector!,
90+
vector.u,
91+
vector.v,
92+
grid,
93+
u_cc,
94+
v_cc,
95+
)
96+
return nothing
97+
end
98+
99+
"""
100+
_rotate_vector!(τx, τy, grid)
101+
102+
Rotate the velocities from the extrinsic coordinate system to the intrinsic
103+
coordinate system.
104+
"""
105+
@kernel function _rotate_vector!(τx, τy, grid)
106+
# Use `k = 1` to index into the reduced Fields
107+
i, j = @index(Global, NTuple)
108+
# Rotate u, v from extrinsic to intrinsic coordinate system
109+
τxr, τyr = OC.Operators.intrinsic_vector(i, j, 1, grid, τx, τy)
110+
@inbounds begin
111+
τx[i, j, 1] = τxr
112+
τy[i, j, 1] = τyr
113+
end
114+
end
115+
116+
"""
117+
_interpolate_vector!(τx, τy, grid, τx_cc, τy_cc)
118+
119+
Interpolate the input fluxes `τx_cc` and `τy_cc`, which are Center/Center
120+
Fields to Face/Center and Center/Face coordinates, respectively.
121+
"""
122+
@kernel function _interpolate_vector!(τx, τy, grid, τx_cc, τy_cc)
123+
# Use `k = 1` to index into the reduced Fields
124+
i, j = @index(Global, NTuple)
125+
@inbounds begin
126+
τx[i, j, 1] = OC.Operators.ℑxᶠᵃᵃ(i, j, 1, grid, τx_cc)
127+
τy[i, j, 1] = OC.Operators.ℑyᵃᶠᵃ(i, j, 1, grid, τy_cc)
128+
end
129+
end

experiments/ClimaEarth/components/ocean/oceananigans.jl

Lines changed: 2 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import Thermodynamics as TD
77
import ClimaOcean.EN4: download_dataset
88
using KernelAbstractions: @kernel, @index, @inbounds
99

10+
include("climaocean_helpers.jl")
11+
1012
"""
1113
OceananigansSimulation{SIM, A, OPROP, REMAP}
1214
@@ -222,70 +224,6 @@ end
222224
Interfacer.step!(sim::OceananigansSimulation, t) =
223225
OC.time_step!(sim.ocean, float(t) - sim.ocean.model.clock.time)
224226

225-
# We always want the surface, so we always set zero(pt.lat) for z
226-
"""
227-
to_node(pt::CA.ClimaCore.Geometry.LatLongPoint)
228-
229-
Transform `LatLongPoint` into a tuple (long, lat, 0), where the 0 is needed because we only
230-
care about the surface.
231-
"""
232-
@inline to_node(pt::CA.ClimaCore.Geometry.LatLongPoint) = pt.long, pt.lat, zero(pt.lat)
233-
# This next one is needed if we have "LevelGrid"
234-
@inline to_node(pt::CA.ClimaCore.Geometry.LatLongZPoint) = pt.long, pt.lat, zero(pt.lat)
235-
236-
"""
237-
map_interpolate(points, oc_field::OC.Field)
238-
239-
Interpolate the given 3D field onto the target points.
240-
241-
If the underlying grid does not contain a given point, return 0 instead.
242-
243-
TODO: Use a non-allocating version of this function (simply replace `map` with `map!`)
244-
"""
245-
function map_interpolate(points, oc_field::OC.Field)
246-
loc = map(L -> L(), OC.Fields.location(oc_field))
247-
grid = oc_field.grid
248-
data = oc_field.data
249-
250-
# TODO: There has to be a better way
251-
min_lat, max_lat = extrema(OC.φnodes(grid, OC.Center(), OC.Center(), OC.Center()))
252-
253-
map(points) do pt
254-
FT = eltype(pt)
255-
256-
# The oceananigans grid does not cover the entire globe, so we should not
257-
# interpolate outside of its latitude bounds. Instead we return 0
258-
min_lat < pt.lat < max_lat || return FT(0)
259-
260-
fᵢ = OC.Fields.interpolate(to_node(pt), data, loc, grid)
261-
convert(FT, fᵢ)::FT
262-
end
263-
end
264-
265-
"""
266-
surface_flux(f::OC.AbstractField)
267-
268-
Extract the top boundary conditions for the given field.
269-
"""
270-
function surface_flux(f::OC.AbstractField)
271-
top_bc = f.boundary_conditions.top
272-
if top_bc isa OC.BoundaryCondition{<:OC.BoundaryConditions.Flux}
273-
return top_bc.condition
274-
else
275-
return nothing
276-
end
277-
end
278-
279-
function Interfacer.remap(field::OC.Field, target_space)
280-
return map_interpolate(CC.Fields.coordinate_field(target_space), field)
281-
end
282-
283-
function Interfacer.remap(operation::OC.AbstractOperations.AbstractOperation, target_space)
284-
evaluated_field = OC.Field(operation)
285-
OC.compute!(evaluated_field)
286-
return Interfacer.remap(evaluated_field, target_space)
287-
end
288-
289227
Interfacer.get_field(sim::OceananigansSimulation, ::Val{:area_fraction}) = sim.area_fraction
290228

291229
# TODO: Better values for this
@@ -390,70 +328,6 @@ function FluxCalculator.update_turbulent_fluxes!(sim::OceananigansSimulation, fi
390328
return nothing
391329
end
392330

393-
"""
394-
set_from_extrinsic_vectors!(vectors, grid, u_cc, v_cc)
395-
396-
Given the extrinsic vector components `u_cc` and `v_cc` as `Center, Center`
397-
fields, rotate them onto the target grid and remap to `Face, Center` and
398-
`Center, Face` fields, respectively.
399-
"""
400-
function set_from_extrinsic_vectors!(vectors, grid, u_cc, v_cc)
401-
arch = grid.architecture
402-
403-
# Rotate vectors onto the grid
404-
OC.Utils.launch!(arch, grid, :xy, _rotate_velocities!, u_cc, v_cc, grid)
405-
406-
# Fill halo regions with the rotated vectors so we can use them to interpolate
407-
OC.fill_halo_regions!(u_cc)
408-
OC.fill_halo_regions!(v_cc)
409-
410-
# Interpolate the vectors to face/center and center/face respectively
411-
OC.Utils.launch!(
412-
arch,
413-
grid,
414-
:xy,
415-
_interpolate_velocities!,
416-
vectors.u,
417-
vectors.v,
418-
grid,
419-
u_cc,
420-
v_cc,
421-
)
422-
return nothing
423-
end
424-
425-
"""
426-
_rotate_velocities!(u, v, grid)
427-
428-
Rotate the velocities from the extrinsic coordinate system to the intrinsic
429-
coordinate system.
430-
"""
431-
@kernel function _rotate_velocities!(u, v, grid)
432-
# Use `k = 1` to index into the reduced Fields
433-
i, j = @index(Global, NTuple)
434-
# Rotate u, v from extrinsic to intrinsic coordinate system
435-
ur, vr = OC.Operators.intrinsic_vector(i, j, 1, grid, u, v)
436-
@inbounds begin
437-
u[i, j, 1] = ur
438-
v[i, j, 1] = vr
439-
end
440-
end
441-
442-
"""
443-
_interpolate_velocities!(u, v, grid, u_cc, v_cc)
444-
445-
Interpolate the input velocities `u_cc` and `v_cc`, which are Center/Center
446-
Fields to Face/Center and Center/Face coordinates, respectively.
447-
"""
448-
@kernel function _interpolate_velocities!(u, v, grid, u_cc, v_cc)
449-
# Use `k = 1` to index into the reduced Fields
450-
i, j = @index(Global, NTuple)
451-
@inbounds begin
452-
u[i, j, 1] = OC.Operators.ℑxyᶠᶜᵃ(i, j, 1, grid, u_cc)
453-
v[i, j, 1] = OC.Operators.ℑxyᶜᶠᵃ(i, j, 1, grid, v_cc)
454-
end
455-
end
456-
457331
function Interfacer.update_field!(sim::OceananigansSimulation, ::Val{:area_fraction}, field)
458332
sim.area_fraction .= field
459333
return nothing

experiments/ClimaEarth/components/ocean/prescr_seaice.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ end
167167
# extensions required by Interfacer
168168
Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:area_fraction}) =
169169
sim.integrator.p.area_fraction
170+
Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:ice_concentration}) =
171+
sim.integrator.p.area_fraction
170172
Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:emissivity}) =
171-
sim.integrator.p.params.ϵ
173+
sim.integrator.p.params.ϵ
172174
Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:roughness_buoyancy}) =
173175
sim.integrator.p.params.z0b
174176
Interfacer.get_field(sim::PrescribedIceSimulation, ::Val{:roughness_momentum}) =

0 commit comments

Comments
 (0)