Skip to content

Commit dfaf00a

Browse files
committed
add ClimaSeaIce simulation [skip ci]
1 parent 3941a53 commit dfaf00a

File tree

7 files changed

+499
-24
lines changed

7 files changed

+499
-24
lines changed

experiments/ClimaEarth/cli_options.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ function argparse_settings()
175175
help = "Directory containing ERA5 initial condition files (subseasonal mode). Filenames inferred from start_date [none (default)]. Generated with `https://github.com/CliMA/WeatherQuest`"
176176
arg_type = String
177177
default = nothing
178+
# Ice model specific
179+
"--ice_model"
180+
help = "Sea ice model to use. [`prescribed` (default), `clima_seaice`]"
181+
arg_type = String
182+
default = "prescribed"
178183
end
179184
return s
180185
end
Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
import Oceananigans as OC
2+
import ClimaSeaIce as CSI
3+
import ClimaOcean as CO
4+
import ClimaCoupler: Checkpointer, FieldExchanger, FluxCalculator, Interfacer, Utilities
5+
import ClimaComms
6+
import ClimaCore as CC
7+
import Thermodynamics as TD
8+
import ClimaOcean.EN4: download_dataset
9+
using KernelAbstractions: @kernel, @index, @inbounds
10+
11+
include("climaocean_helpers.jl")
12+
13+
"""
14+
ClimaSeaIceSimulation{SIM, A, OPROP, REMAP}
15+
16+
The ClimaCoupler simulation object used to run with ClimaSeaIce.
17+
This type is used by the coupler to indicate that this simulation
18+
is an surface/ocean simulation for dispatch.
19+
20+
It contains the following objects:
21+
- `ice::SIM`: The ClimaSeaIce simulation object.
22+
- `area_fraction::A`: A ClimaCore Field representing the surface area fraction of this component model on the exchange grid.
23+
- `melting_speed::MS`: An constant characteristic speed for melting/freezing.
24+
- `remapping::REMAP`: Objects needed to remap from the exchange (spectral) grid to Oceananigans spaces.
25+
- `ocean_ice_fluxes::NT`: A NamedTuple of fluxes between the ocean and sea ice, computed at each coupling step.
26+
"""
27+
struct ClimaSeaIceSimulation{SIM, A, MS, REMAP, NT} <: Interfacer.SeaIceModelSimulation
28+
ice::SIM
29+
area_fraction::A
30+
melting_speed::MS
31+
remapping::REMAP
32+
ocean_ice_fluxes::NT
33+
end
34+
35+
"""
36+
ClimaSeaIceSimulation()
37+
38+
Creates an ClimaSeaIceSimulation object containing a model, an integrator, and
39+
a surface area fraction field.
40+
This type is used to indicate that this simulation is an ocean simulation for
41+
dispatch in coupling.
42+
43+
Initially, no sea ice is present.
44+
45+
Since this model does not solve for prognostic temperature, we use a
46+
prescribed heat flux boundary condition at the top, which is used to solve for
47+
temperature at the surface of the sea ice. The surface temperature from the
48+
previous step is provided to the coupler to be used in computing fluxes.
49+
50+
Specific details about the default model configuration
51+
can be found in the documentation for `ClimaOcean.ocean_simulation`.
52+
"""
53+
function ClimaSeaIceSimulation(land_fraction, ocean; output_dir)
54+
# Initialize the sea ice with the same grid as the ocean
55+
grid = ocean.ocean.model.grid # TODO can't use lat/lon grid at poles for ice, need to fill with bucket
56+
arch = OC.Architectures.architecture(grid)
57+
advection = ocean.ocean.model.advection.T
58+
top_heat_boundary_condition = CSI.MeltingConstrainedFluxBalance()
59+
60+
# TODO use ClimaOcean 0.8.6
61+
ice = CO.sea_ice_simulation(grid, ocean.ocean; advection, top_heat_boundary_condition)
62+
63+
melting_speed = 1e-4
64+
65+
# Since ocean and sea ice share the same grid, we can also share the remapping objects
66+
remapping = ocean.remapping
67+
68+
# Before version 0.96.22, the NetCDFWriter was broken on GPU
69+
if arch isa OC.CPU || pkgversion(OC) >= v"0.96.22"
70+
# Save all tracers and velocities to a NetCDF file at daily frequency
71+
outputs = OC.prognostic_fields(ice.model)
72+
jld_writer = OC.JLD2Writer(
73+
ice.model,
74+
outputs;
75+
schedule = OC.TimeInterval(86400), # Daily output
76+
filename = joinpath(output_dir, "seaice_diagnostics.jld2"),
77+
overwrite_existing = true,
78+
array_type = Array{Float32},
79+
)
80+
ice.output_writers[:diagnostics] = jld_writer
81+
end
82+
83+
# Allocate space for the sea ice-ocean (io) fluxes
84+
io_bottom_heat_flux = OC.Field{OC.Center, OC.Center, Nothing}(grid)
85+
io_frazil_heat_flux = OC.Field{OC.Center, OC.Center, Nothing}(grid)
86+
io_salt_flux = OC.Field{OC.Center, OC.Center, Nothing}(grid)
87+
x_momentum = OC.Field{OC.Face, OC.Center, Nothing}(grid)
88+
y_momentum = OC.Field{OC.Center, OC.Face, Nothing}(grid)
89+
90+
ocean_ice_fluxes = (
91+
interface_heat = io_bottom_heat_flux,
92+
frazil_heat = io_frazil_heat_flux,
93+
salt = io_salt_flux,
94+
x_momentum = x_momentum,
95+
y_momentum = y_momentum,
96+
)
97+
98+
# Get the initial area fraction from the fractional ice concentration
99+
boundary_space = axes(ocean.area_fraction)
100+
FT = CC.Spaces.undertype(boundary_space)
101+
area_fraction = Interfacer.remap(ice.model.ice_concentration, boundary_space)
102+
103+
# Overwrite ice fraction with the static land area fraction anywhere we have nonzero land area
104+
# max needed to avoid Float32 errors (see issue #271; Heisenbug on HPC)
105+
@. area_fraction = max(min(area_fraction, FT(1) - land_fraction), FT(0))
106+
107+
sim = ClimaSeaIceSimulation(
108+
ice,
109+
area_fraction,
110+
melting_speed,
111+
remapping,
112+
ocean_ice_fluxes,
113+
)
114+
return sim
115+
end
116+
117+
###############################################################################
118+
### Functions required by ClimaCoupler.jl for a SurfaceModelSimulation
119+
###############################################################################
120+
121+
# Timestep the simulation forward to time `t`
122+
Interfacer.step!(sim::ClimaSeaIceSimulation, t) =
123+
OC.time_step!(sim.ice, float(t) - sim.ice.model.clock.time)
124+
125+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:area_fraction}) = sim.area_fraction
126+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:ice_concentration}) =
127+
sim.ice.model.ice_concentration
128+
129+
# At the moment, we return always Float32. This is because we always want to run
130+
# Oceananingans with Float64, so we have no way to know the float type here. Sticking with
131+
# Float32 ensures that nothing is accidentally promoted to Float64. We will need to change
132+
# this anyway.
133+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:roughness_buoyancy}) =
134+
Float32(5.8e-5)
135+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:roughness_momentum}) =
136+
Float32(5.8e-5)
137+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:beta}) = Float32(1)
138+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:emissivity}) = Float32(1)
139+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:surface_direct_albedo}) =
140+
Float32(0.7)
141+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:surface_diffuse_albedo}) =
142+
Float32(0.7)
143+
144+
# Approximate the sea ice surface temperature as the temperature computed from the
145+
# fluxes at the previous timestep.
146+
Interfacer.get_field(sim::ClimaSeaIceSimulation, ::Val{:surface_temperature}) =
147+
273.15 + sim.ice.model.ice_thermodynamics.top_surface_temperature
148+
149+
"""
150+
FluxCalculator.update_turbulent_fluxes!(sim::ClimaSeaIceSimulation, fields)
151+
152+
Update the turbulent fluxes in the simulation using the values stored in the coupler fields.
153+
These include latent heat flux, sensible heat flux, momentum fluxes, and moisture flux.
154+
155+
The input `fields` are already area-weighted, so there's no need to weight them again.
156+
157+
Note that currently the moisture flux has no effect on the sea ice model, which has
158+
constant salinity.
159+
160+
A note on sign conventions:
161+
SurfaceFluxes and ClimaSeaIce both use the convention that a positive flux is an upward flux.
162+
No sign change is needed during the exchange, except for moisture/salinity fluxes:
163+
SurfaceFluxes provides moisture moving from atmosphere to ocean as a negative flux at the surface,
164+
and ClimaSeaIce represents moisture moving from atmosphere to ocean as a positive salinity flux,
165+
so a sign change is needed when we convert from moisture to salinity flux.
166+
"""
167+
function FluxCalculator.update_turbulent_fluxes!(sim::ClimaSeaIceSimulation, fields)
168+
# Only LatitudeLongitudeGrid are supported because otherwise we have to rotate the vectors
169+
170+
(; F_lh, F_sh, F_turb_ρτxz, F_turb_ρτyz, F_turb_moisture) = fields
171+
grid = sim.ice.model.grid
172+
173+
# Remap momentum fluxes onto reduced 2D Center, Center fields using scratch arrays and fields
174+
CC.Remapping.interpolate!(
175+
sim.remapping.scratch_arr1,
176+
sim.remapping.remapper_cc,
177+
F_turb_ρτxz,
178+
)
179+
OC.set!(sim.remapping.scratch_cc1, sim.remapping.scratch_arr1) # zonal momentum flux
180+
CC.Remapping.interpolate!(
181+
sim.remapping.scratch_arr2,
182+
sim.remapping.remapper_cc,
183+
F_turb_ρτyz,
184+
)
185+
OC.set!(sim.remapping.scratch_cc2, sim.remapping.scratch_arr2) # meridional momentum flux
186+
187+
# Rename for clarity; these are now Center, Center Oceananigans fields
188+
F_turb_ρτxz_cc = sim.remapping.scratch_cc1
189+
F_turb_ρτyz_cc = sim.remapping.scratch_cc2
190+
191+
# Set the momentum flux BCs at the correct locations using the remapped scratch fields
192+
# Note that this requires the sea ice model to always be run with dynamics turned on
193+
si_flux_u = sim.ice.model.dynamics.external_momentum_stresses.top.u
194+
si_flux_v = sim.ice.model.dynamics.external_momentum_stresses.top.v
195+
set_from_extrinsic_vectors!(
196+
(; u = si_flux_u, v = si_flux_v),
197+
grid,
198+
F_turb_ρτxz_cc,
199+
F_turb_ρτyz_cc,
200+
)
201+
202+
# Remap the latent and sensible heat fluxes using scratch arrays
203+
CC.Remapping.interpolate!(sim.remapping.scratch_arr1, sim.remapping.remapper_cc, F_lh) # latent heat flux
204+
CC.Remapping.interpolate!(sim.remapping.scratch_arr2, sim.remapping.remapper_cc, F_sh) # sensible heat flux
205+
206+
# Rename for clarity; recall F_turb_energy = F_lh + F_sh
207+
remapped_F_lh = sim.remapping.scratch_arr1
208+
remapped_F_sh = sim.remapping.scratch_arr2
209+
210+
# Update the sea ice only where the concentration is greater than zero.
211+
si_flux_heat = ice_sim.ice.model.external_heat_fluxes.top
212+
OC.interior(si_flux_heat, :, :, 1) .=
213+
OC.interior(si_flux_heat, :, :, 1) .+ (remapped_F_lh .+ remapped_F_sh)
214+
215+
return nothing
216+
end
217+
218+
function Interfacer.update_field!(sim::ClimaSeaIceSimulation, ::Val{:area_fraction}, field)
219+
sim.area_fraction .= field
220+
return nothing
221+
end
222+
223+
"""
224+
FieldExchanger.update_sim!(sim::ClimaSeaIceSimulation, csf, area_fraction)
225+
226+
Update the sea ice simulation with the provided fields, which have been filled in
227+
by the coupler.
228+
229+
Update the portion of the surface_fluxes for T and S that is due to radiation and
230+
precipitation. The rest will be updated in `update_turbulent_fluxes!`.
231+
232+
Note that currently precipitation has no effect on the sea ice model, which has
233+
constant salinity.
234+
235+
A note on sign conventions:
236+
ClimaAtmos and ClimaSeaIce both use the convention that a positive flux is an upward flux.
237+
No sign change is needed during the exchange, except for precipitation/salinity fluxes.
238+
ClimaAtmos provides precipitation as a negative flux at the surface, and
239+
ClimaSeaIce represents precipitation as a positive salinity flux,
240+
so a sign change is needed when we convert from precipitation to salinity flux.
241+
"""
242+
function FieldExchanger.update_sim!(sim::ClimaSeaIceSimulation, csf, area_fraction)
243+
# Remap radiative flux onto scratch array; rename for clarity
244+
CC.Remapping.interpolate!(
245+
sim.remapping.scratch_arr1,
246+
sim.remapping.remapper_cc,
247+
csf.F_radiative,
248+
)
249+
remapped_F_radiative = sim.remapping.scratch_arr1
250+
251+
# Update only the part due to radiative fluxes. For the full update, the component due
252+
# to latent and sensible heat is missing and will be updated in update_turbulent_fluxes.
253+
si_flux_heat = sim.ice.model.external_heat_fluxes.top
254+
OC.interior(si_flux_heat, :, :, 1) .= remapped_F_radiative
255+
256+
return nothing
257+
end
258+
259+
"""
260+
ocean_seaice_fluxes!(ocean_sim::OceananigansSimulation, ice_sim::ClimaSeaIceSimulation)
261+
262+
Compute the fluxes between the ocean and sea ice, storing them in the `ocean_ice_fluxes`
263+
fields of the ocean and sea ice simulations.
264+
265+
!!! note
266+
This function must be called after the turbulent fluxes have been updated in both
267+
simulations. Here only the contributions from the sea ice/ocean interactions
268+
are added to the fluxes.
269+
"""
270+
function FluxCalculator.ocean_seaice_fluxes!(
271+
ocean_sim::OceananigansSimulation,
272+
ice_sim::ClimaSeaIceSimulation,
273+
)
274+
melting_speed = ice_sim.melting_speed
275+
ocean_properties = ocean_sim.ocean_properties
276+
ice_concentration = Interfacer.get_field(ice_sim, Val(:ice_concentration))
277+
278+
# Compute the fluxes and store them in the both simulations
279+
ocean_properties = (;
280+
reference_density = ocean_properties.ocean_reference_density,
281+
heat_capacity = ocean_properties.ocean_heat_capacity,
282+
) # TODO rename in constructor
283+
CO.OceanSeaIceModels.InterfaceComputations.compute_sea_ice_ocean_fluxes!(
284+
ice_sim.ocean_ice_fluxes,
285+
ocean_sim.ocean,
286+
ice_sim.ice,
287+
melting_speed,
288+
ocean_properties,
289+
)
290+
291+
## Update the internals of the sea ice model
292+
# Set the bottom heat flux to the sum of the frazil and interface heat fluxes
293+
bottom_heat_flux = ice_sim.ice.model.external_heat_fluxes.bottom
294+
295+
Qf = ice_sim.ocean_ice_fluxes.frazil_heat # frazil heat flux
296+
Qi = ice_sim.ocean_ice_fluxes.interface_heat # interfacial heat flux
297+
bottom_heat_flux .= Qf .+ Qi
298+
299+
## Update the internals of the ocean model
300+
ρₒ⁻¹ = 1 / ocean_sim.ocean_properties.ocean_reference_density
301+
cₒ = ocean_sim.ocean_properties.ocean_heat_capacity
302+
303+
# Compute fluxes for u, v, T, and S from momentum, heat, and freshwater fluxes
304+
oc_flux_u = surface_flux(ocean_sim.ocean.model.velocities.u)
305+
oc_flux_v = surface_flux(ocean_sim.ocean.model.velocities.v)
306+
307+
ρτxio = ice_sim.ocean_ice_fluxes.x_momentum # sea_ice - ocean zonal momentum flux
308+
ρτyio = ice_sim.ocean_ice_fluxes.y_momentum # sea_ice - ocean meridional momentum flux
309+
310+
# Update the momentum flux contributions from ocean/sea ice fluxes
311+
grid = ocean_sim.ocean.model.grid
312+
arch = OC.Architectures.architecture(grid)
313+
OC.Utils.launch!(
314+
arch,
315+
grid,
316+
:xy,
317+
_add_ocean_ice_stress!,
318+
oc_flux_u,
319+
oc_flux_v,
320+
grid,
321+
ρτxio,
322+
ρτyio,
323+
ρₒ⁻¹,
324+
ice_concentration,
325+
)
326+
327+
oc_flux_T = surface_flux(ocean_sim.ocean.model.tracers.T)
328+
OC.interior(oc_flux_T, :, :, 1) .+=
329+
OC.interior(ice_concentration, :, :, 1) .* OC.interior(Qi, :, :, 1) .* ρₒ⁻¹ ./ cₒ
330+
331+
oc_flux_S = surface_flux(ocean_sim.ocean.model.tracers.S)
332+
OC.interior(oc_flux_S, :, :, 1) .+=
333+
OC.interior(ice_concentration, :, :, 1) .*
334+
OC.interior(ice_sim.ocean_ice_fluxes.salt, :, :, 1)
335+
336+
return nothing
337+
end
338+
339+
@kernel function _add_ocean_ice_stress!(
340+
oc_flux_u,
341+
oc_flux_v,
342+
grid,
343+
ρτxio,
344+
ρτyio,
345+
ρₒ⁻¹,
346+
ice_concentration,
347+
)
348+
i, j = @index(Global, NTuple)
349+
350+
# ℑxᶠᵃᵃ: interpolate faces to centers
351+
oc_flux_u +=
352+
ρτxio[i, j, 1] * ρₒ⁻¹ * OC.Operators.ℑxᶠᵃᵃ(i, j, 1, grid, ice_concentration)
353+
oc_flux_v +=
354+
ρτyio[i, j, 1] * ρₒ⁻¹ * OC.Operators.ℑyᵃᶠᵃ(i, j, 1, grid, ice_concentration)
355+
end
356+
357+
"""
358+
get_model_prog_state(sim::ClimaSeaIceSimulation)
359+
360+
Returns the model state of a simulation as a `ClimaCore.FieldVector`.
361+
It's okay to leave this unimplemented for now, but we won't be able to use the
362+
restart system.
363+
364+
TODO extend this for non-ClimaCore states.
365+
"""
366+
function Checkpointer.get_model_prog_state(sim::ClimaSeaIceSimulation)
367+
@warn "get_model_prog_state not implemented for ClimaSeaIceSimulation"
368+
end

0 commit comments

Comments
 (0)