|
| 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